diff --git a/.gitignore b/.gitignore index 24ceaf7..302ee8c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,4 @@ *~ -vendor -.bundle .sass-cache _site .DS_Store @@ -8,10 +6,3 @@ _site .settings .classpath .metadata -.DS_Store -Thumbs.db -db.json -*.log -node_modules/ -public/ -.deploy*/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index b963fc4..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "themes/yelee"] - path = themes/yelee - url = git@github.com:allenn/hexo-theme-yelee.git -[submodule "themes/next"] - path = themes/next -url = git@github.com:allenn/hexo-theme-next.git diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 063b0e4..0000000 --- a/.npmignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store -Thumbs.db -db.json -*.log -node_modules/ -public/ -.deploy*/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac2f510..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -language: node_js -node_js: -- 'stable' - -sudo: false -git: - submodules: false -branches: - only: - - hexo -cache: - directories: - - node_modules -before_install: -- openssl aes-256-cbc -K $encrypted_b60764d3cdc1_key -iv $encrypted_b60764d3cdc1_iv -in .travis/travis.enc -out ~/.ssh/id_rsa -d -- chmod 600 ~/.ssh/id_rsa -- eval $(ssh-agent) -- ssh-add ~/.ssh/id_rsa -- cp .travis/ssh_config ~/.ssh/config -- git config --global user.name 'Xudong Zheng' -- git config --global user.email 'xudongzheng1225@gmail.com' -- sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules -- git submodule update --init --recursive -- git clone -b master git@github.com:allenn/allenn.github.io.git .deploy_git -- cp -r .deploy_git public -- rm -rf ./public/.git -- export TZ='Asia/Shanghai' -install: -- npm install hexo-cli -g -- npm install -script: -- hexo g -- hexo d diff --git a/.travis/ssh_config b/.travis/ssh_config deleted file mode 100644 index 16afe76..0000000 --- a/.travis/ssh_config +++ /dev/null @@ -1,10 +0,0 @@ -Host github.com - User git - StrictHostKeyChecking no - IdentityFile ~/.ssh/id_rsa - IdentitiesOnly yes -Host git.coding.net - User git - StrictHostKeyChecking no - IdentityFile ~/.ssh/id_rsa - IdentitiesOnly yes diff --git a/.travis/travis.enc b/.travis/travis.enc deleted file mode 100644 index e7ca158..0000000 Binary files a/.travis/travis.enc and /dev/null differ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100755 index 0000000..af5cc9d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2015 Allen Zheng + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md old mode 100644 new mode 100755 index efac7fe..e69de29 --- a/README.md +++ b/README.md @@ -1 +0,0 @@ - diff --git a/_config.yml b/_config.yml deleted file mode 100755 index ce2a29d..0000000 --- a/_config.yml +++ /dev/null @@ -1,88 +0,0 @@ -# Hexo Configuration -## Docs: https://hexo.io/docs/configuration.html -## Source: https://github.com/hexojs/hexo/ - -# Site -title: Allen's Blog -subtitle: -description: -author: Allen Zheng -email: xudongzheng1225@gmail.com -language: zh-Hans -timezone: - -# URL -## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/' -url: http://allenn.cn -root: / -permalink: articles/:year-:month/:title/ -permalink_defaults: - -# Directory -source_dir: source -public_dir: public -tag_dir: tags -archive_dir: archives -category_dir: categories -code_dir: downloads/code -i18n_dir: :lang -skip_render: - -# Writing -new_post_name: :title.md # File name of new posts -default_layout: post -titlecase: false # Transform title into titlecase -external_link: true # Open external links in new tab -filename_case: 0 -render_drafts: false -post_asset_folder: false -relative_link: false -future: true -highlight: - enable: true - line_number: false - auto_detect: false - tab_replace: true - -# Category & Tag -default_category: uncategorized -category_map: -tag_map: - -# Date / Time format -## Hexo uses Moment.js to parse and display date -## You can customize the date format as defined in -## http://momentjs.com/docs/#/displaying/format/ -date_format: YYYY-MM-DD -time_format: HH:mm:ss - -# Pagination -## Set per_page to 0 to disable pagination -per_page: 10 -pagination_dir: page - -feed: - type: atom - path: atom.xml - limit: 20 - hub: - -#sitemap configuration -sitemap: - path: sitemap.xml - -baidusitemap: - path: baidusitemap.xml -# Extensions -## Plugins: https://hexo.io/plugins/ -## Themes: https://hexo.io/themes/ -#theme: landscape -#theme: yilia -theme: next -# Deployment -## Docs: https://hexo.io/docs/deployment.html -deploy: - type: git - repo: - github: git@github.com:allenn/allenn.github.io.git,master - coding: git@git.coding.net:allennn/allennn.git,coding-pages diff --git a/source/_posts/2015-10-14-powermockito-would-not-work-on-multi-thread.md b/_posts/2015-10-14-powermockito-would-not-work-on-multi-thread.md similarity index 72% rename from source/_posts/2015-10-14-powermockito-would-not-work-on-multi-thread.md rename to _posts/2015-10-14-powermockito-would-not-work-on-multi-thread.md index 6c59733..3f6afc8 100644 --- a/source/_posts/2015-10-14-powermockito-would-not-work-on-multi-thread.md +++ b/_posts/2015-10-14-powermockito-would-not-work-on-multi-thread.md @@ -3,20 +3,17 @@ layout: post title: "在多线程构建场景下Powermockito无法在不同类中Mock同一个静态方法" date: 2015-10-14 13:50:39 comments: true -ads: true categories: 软件技术 -tags: - - Java - - 单元测试 +tags: [Java, 单元测试] --- -在修改单元测试的过程中,不幸踩了个坑,发现 Powermockito 的PowerMock.mockStatic(ClassThatContainsStaticMethod.class) 在多线程场景下是无法正常工作的,这再次验证了之前 ThrougthWorks 顾问说的那句话: +在修改单元测试的过程中,不幸踩了个坑,发现**Powermockito**的``` PowerMock.mockStatic(ClassThatContainsStaticMethod.class)```在多线程场景下是无法正常工作的,这再次验证了之前ThrougthWorks顾问说的那句话: >除非万不得已,或者是Mock遗留系统接口,否则不要使用Powermockito。 -发生问题的场景是这样的 Class C 有一个静态方法,Class A 和 Class B 都需要调用这个方法完成一些功能: +发生问题的场景是这样的```Class C```有一个静态方法,```Class A```和```Class B```都需要调用这个方法完成一些功能: ```java Class C{ @@ -38,7 +35,7 @@ Class B { } ``` -由于在测试中直接调用 C.getSomeObject() 会导致一些不可预期的错误,所以我想对AB类进行测试就必须使用Mock,于是我那么写: +由于在测试中直接调用```C.getSomeObject()```会导致一些不可预期的错误,所以我想对AB类进行测试就必须使用Mock,于是我那么写: ```java Class ATest{ @@ -61,7 +58,7 @@ Class BTest{ ``` -当我在IDE中分别运行 ATest 或者 BTest 是,我的测试都是能正确运行的,但是当你使用Maven或者其他的构建工具进行多线程测试的时候,你就会发现问题来了。一会是A抛异常,一会是B抛异常,总之就是不能很好的工作。由于我不是Powermockito的专家,所以无法深入的去探究这个问题的原因,但是我想,这应该是和静态方法本身在一个JVM内的唯一性有关,我截取了网上两个解释供参考: +当我在IDE中分别运行```ATest```或者```BTest```是,我的测试都是能正确运行的,但是当你使用Maven或者其他的构建工具进行多线程测试的时候,你就会发现问题来了。一会是A抛异常,一会是B抛异常,总之就是不能很好的工作。由于我不是Powermockito的专家,所以无法深入的去探究这个问题的原因,但是我想,这应该是和静态方法本身在一个JVM内的唯一性有关,我截取了网上两个解释供参考: #### Explanation 1 diff --git a/source/_posts/2015-10-14-unit-test-mock.md b/_posts/2015-10-14-unit-test-mock.md similarity index 96% rename from source/_posts/2015-10-14-unit-test-mock.md rename to _posts/2015-10-14-unit-test-mock.md index d267d6f..d99b658 100644 --- a/source/_posts/2015-10-14-unit-test-mock.md +++ b/_posts/2015-10-14-unit-test-mock.md @@ -3,26 +3,23 @@ layout: post title: "Mock 与 Stub" date: 2015-10-14 16:50:39 comments: true -ads: true categories: 软件技术 -tags: - - Java - - 单元测试 +tags: [Java, 单元测试] --- >在进行单元测试的时候,我们会发现我们要测试的方法会有很多外部依赖,比如:发邮件,进行网络通讯,操作文件系统等等。而我们通常关注的是被测试对象的功能和行为,对于它的依赖,我们仅仅需要关注它们之间的交互,但对依赖的对象是如何执行的具体细节我们并不关注。较为常见的技巧就是使用mock对象或者stub对象来代替真实的依赖。 -## Mocks aren't stubs +##Mocks aren't stubs 这是软件大师[Martin Fowler](http://martinfowler.com/)的一篇经典博文。Martin大师在文章中详细的解释了Mock与Stub的区别,以及怎样使用它们进行TDD实践等等一系列干货,强烈推荐阅读,猛击[这里](http://martinfowler.com/articles/mocksArentStubs.html)阅读原文。我无意把大师的话再复述一遍,所以在本文中我就聊聊我对Mock与Stub的理解以及一些实践。 -### 相同点 +###相同点 先看看两者的相同点吧,非常明确的是,Mock和Stub都可以用来对系统(或者将粒度放小为模块,单元)进行隔离。先看看两者的相同点吧,非常明确的是,Mock和Stub都可以用来对系统(或者将粒度放小为模块,单元)进行隔离。 -### 不同点 +###不同点 Mock和Stub有两个主要区别: @@ -31,7 +28,7 @@ Mock和Stub有两个主要区别: 上面的说法比较抽象,让我们通过例子来看看Mock与Stub的区别。 -## 使用Stub进行单元测试 +##使用Stub进行单元测试 下面是一个使用Stub进行单元测试的例子,我们打算创建一个订单对象,并用仓库中的货物填充这个订单。这个订单对象很简单,只有产品和数量两种信息,仓库保存着不同产品的目录。当我们需要填充订单的时候,会有两种不同的回应,如果仓库中有足够的货物,那么订单就会被填满,并且仓库相应产品的数量就会降低到对应的数量。如果仓库中没有足够的参评,那么订单就不会被填充,并且仓库中产品的数量没有任何的变化。 diff --git a/source/_posts/2016-08-10-java-CompletionService-tips.md b/_posts/2016-08-10-java-CompletionService-tips.md similarity index 99% rename from source/_posts/2016-08-10-java-CompletionService-tips.md rename to _posts/2016-08-10-java-CompletionService-tips.md index 839950e..3ac403e 100644 --- a/source/_posts/2016-08-10-java-CompletionService-tips.md +++ b/_posts/2016-08-10-java-CompletionService-tips.md @@ -3,7 +3,6 @@ layout: post title: "CompletionService小技巧" date: 2016-08-10 16:50:39 comments: true -ads: true categories: 软件技术 tags: [Java, 多线程] --- diff --git a/source/_posts/2016-08-12-intro-pomodoro-technique.md b/_posts/2016-08-12-intro-pomodoro-technique.md similarity index 93% rename from source/_posts/2016-08-12-intro-pomodoro-technique.md rename to _posts/2016-08-12-intro-pomodoro-technique.md index 70d9e0f..3906b6d 100644 --- a/source/_posts/2016-08-12-intro-pomodoro-technique.md +++ b/_posts/2016-08-12-intro-pomodoro-technique.md @@ -3,13 +3,12 @@ layout: post title: "25分钟改变你的工作效率:番茄工作法" date: 2016-08-12 16:50:39 comments: true -ads: true categories: 工作方法 --- 少年,你是不是每5分钟就会拿起一次手机?你是不是每天忙忙碌碌但最后好像什么也没干?你是不是日复一日的加班感觉身体被掏空? - + 那么,你想变得更专注么?你想变得更有效率么?你想每天下班按时回家么?那么来试试这个番茄工作法吧! @@ -23,11 +22,11 @@ categories: 工作方法 OK,那让我们来实践一下,请选择一件你觉得在25分钟内能完成的工作,在这25分钟的工作时间中,请把Email关掉,手机调整成静音,打好茶水。然后把手机的定时器打开,设置25分钟的倒计时,然后开始工作。 - + OK,Time up,现在感觉怎么样?是不是找回了专注的感觉?*一次只专注做好一件事情*,这就是番茄工作法的精髓所在。我们都知道工作情境的切换特别耗费人的精力,你也许会有过这样的体验,当你在酝酿某种思路时,突然一个电话,你之前在大脑里构建的宏伟建筑顷刻就能灰飞烟灭。你只有一双脚,不能同时跳两场舞。 - + ## 如何做? 「番茄工作法」的使用主流程如下: @@ -47,18 +46,18 @@ OK,Time up,现在感觉怎么样?是不是找回了专注的感觉?*一次 下面用一张图来说明整个番茄工作法的流程,这张图来自[《番茄工作法图解-简单易行的时间管理方法》](https://www.amazon.cn/%E7%95%AA%E8%8C%84%E5%B7%A5%E4%BD%9C%E6%B3%95%E5%9B%BE%E8%A7%A3-%E7%AE%80%E5%8D%95%E6%98%93%E8%A1%8C%E7%9A%84%E6%97%B6%E9%97%B4%E7%AE%A1%E7%90%86%E6%96%B9%E6%B3%95-%E8%AF%BA%E7%89%B9%E4%BC%AF%E6%A0%BC/dp/B004O9F71K): - + ## 技巧和心得 OK,上面说了那么多,相信你对番茄工作法已经有一定的了解了,那为什么我要推荐给你,因为我用了以后觉得好,就像成龙大哥说的那样:我用完之后是这样子,你们用完之后,也是这样子! - + 1. 在进行「番茄钟」时,我们应该尽量想办法避开一些容易分神的信息源。例如:将手机的消息推送静音,关闭 QQ 和微信。相信我,你大多数时候并不需要立即响应聊天软件中的信息。使用「番茄钟」后,你的消息回复时间最坏情况下也就是晚 25 分钟。如果真的有人有急事,他会直接到你的办公桌来找你,或者直接给你打电话的。 2. 刚开始的时候我建议你把「番茄钟」设定在25分钟,随着你的注意力控制越来越容易,你可以适当延长「番茄钟」的时间。就我的经验,延长到 45 - 50 分钟是完全没有问题的。其实你如果明白了「番茄钟」的原理,你甚至可以在心中假想一个「番茄钟」,然后让自己快速进入精力集中状态。 3. 一天下来,回顾自己当天的「番茄钟」完成情况,看看主要的问题在于自己的精力无法集中,还是老是被别人打断。针对具体的问题可以做一些调整的尝试。比如,如果是精力无法集中,则看看是否是干扰源过多,尽量减少干扰。如果是老被打断,则可以和同事商量一些工作方式,比如让同事尽量用邮件和 QQ 找你,而不是当面打扰你。 4. 我使用了一个内置番茄计时器的看板程序来追踪我每天的工作情况,[Kanbanflow](https://kanbanflow.com),我每天会在开完早会后花一个番茄钟的时间来安排我一天的任务,并用它来作为我的「番茄钟」计时器。它还能输出一个报告,告诉你每天完成了多少个「番茄钟」,被打断了多少次,被打断的原因又是什么。我觉得不错,你可以去试试。 - + 最后我想说的是,我们应该认识到,工作被打断是在所难免的,总会有一些临时的沟通工作无法被计划,并且是有价值的。所以,「番茄钟」失败后,不应该有过多的情绪,尽快重启一个「番茄钟」,你的工作应该很快就会进入状态。Try it,相信你也会有很大的收获。 diff --git a/source/_posts/2016-08-14-java-api-need-to-know.md b/_posts/2016-08-14-java-api-need-to-know.md similarity index 99% rename from source/_posts/2016-08-14-java-api-need-to-know.md rename to _posts/2016-08-14-java-api-need-to-know.md index 4842383..a95c3ff 100644 --- a/source/_posts/2016-08-14-java-api-need-to-know.md +++ b/_posts/2016-08-14-java-api-need-to-know.md @@ -3,7 +3,6 @@ layout: post title: "Java核心API需要掌握的程度" date: 2016-08-14 23:21:39 comments: true -ads: true categories: 软件技术 tags: [Java, api] --- diff --git a/source/assets/images/alipay.png b/img/alipay.png similarity index 100% rename from source/assets/images/alipay.png rename to img/alipay.png diff --git a/source/assets/images/blog-img/avatar.jpg b/img/avatar.jpg similarity index 100% rename from source/assets/images/blog-img/avatar.jpg rename to img/avatar.jpg diff --git a/source/assets/images/btn_reward.gif b/img/btn_reward.gif similarity index 100% rename from source/assets/images/btn_reward.gif rename to img/btn_reward.gif diff --git a/source/assets/images/intro-pomodoro-technique/25mins-get-it-done.png b/img/intro-pomodoro-technique/25mins-get-it-done.png similarity index 100% rename from source/assets/images/intro-pomodoro-technique/25mins-get-it-done.png rename to img/intro-pomodoro-technique/25mins-get-it-done.png diff --git a/source/assets/images/intro-pomodoro-technique/KanbanFlowBoard.png b/img/intro-pomodoro-technique/KanbanFlowBoard.png similarity index 100% rename from source/assets/images/intro-pomodoro-technique/KanbanFlowBoard.png rename to img/intro-pomodoro-technique/KanbanFlowBoard.png diff --git a/source/assets/images/intro-pomodoro-technique/duang.png b/img/intro-pomodoro-technique/duang.png similarity index 100% rename from source/assets/images/intro-pomodoro-technique/duang.png rename to img/intro-pomodoro-technique/duang.png diff --git a/source/assets/images/intro-pomodoro-technique/overview.png b/img/intro-pomodoro-technique/overview.png similarity index 100% rename from source/assets/images/intro-pomodoro-technique/overview.png rename to img/intro-pomodoro-technique/overview.png diff --git a/source/assets/images/intro-pomodoro-technique/time-lack.png b/img/intro-pomodoro-technique/time-lack.png similarity index 100% rename from source/assets/images/intro-pomodoro-technique/time-lack.png rename to img/intro-pomodoro-technique/time-lack.png diff --git a/source/assets/images/intro-pomodoro-technique/two-dance.gif b/img/intro-pomodoro-technique/two-dance.gif similarity index 100% rename from source/assets/images/intro-pomodoro-technique/two-dance.gif rename to img/intro-pomodoro-technique/two-dance.gif diff --git a/img/weixin.png b/img/weixin.png new file mode 100644 index 0000000..d39e073 Binary files /dev/null and b/img/weixin.png differ diff --git a/package.json b/package.json deleted file mode 100644 index a206c00..0000000 --- a/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "hexo-site", - "version": "0.0.0", - "private": true, - "hexo": { - "version": "3.3.1" - }, - "dependencies": { - "hexo": "^3.3.1", - "hexo-deployer-git": "^0.2.0", - "hexo-generator-archive": "^0.1.4", - "hexo-generator-baidu-sitemap": "^0.1.2", - "hexo-generator-category": "^0.1.3", - "hexo-generator-feed": "^1.2.0", - "hexo-generator-index": "^0.2.1", - "hexo-generator-seo-friendly-sitemap": "0.0.19", - "hexo-generator-sitemap": "^1.1.2", - "hexo-generator-tag": "^0.2.0", - "hexo-renderer-ejs": "^0.2.0", - "hexo-renderer-marked": "^0.2.10", - "hexo-renderer-stylus": "^0.3.1", - "hexo-server": "^0.2.0" - } -} \ No newline at end of file diff --git a/scaffolds/draft.md b/scaffolds/draft.md deleted file mode 100644 index 498e95b..0000000 --- a/scaffolds/draft.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: {{ title }} -tags: ---- diff --git a/scaffolds/page.md b/scaffolds/page.md deleted file mode 100644 index f01ba3c..0000000 --- a/scaffolds/page.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: {{ title }} -date: {{ date }} ---- diff --git a/scaffolds/post.md b/scaffolds/post.md deleted file mode 100644 index 1f9b9a4..0000000 --- a/scaffolds/post.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: {{ title }} -date: {{ date }} -tags: ---- diff --git a/source/404.html b/source/404.html deleted file mode 100644 index 5b8f15f..0000000 --- a/source/404.html +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: 404 -date: 2016-09-01 22:24:17 ---- - -
- - - - - - - - - - - diff --git a/source/CNAME b/source/CNAME deleted file mode 100644 index 9c41d8c..0000000 --- a/source/CNAME +++ /dev/null @@ -1 +0,0 @@ -allenn.cn diff --git a/source/_drafts/.gitignore b/source/_drafts/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/source/_drafts/hbase-store-structure-rowkey-design.md b/source/_drafts/hbase-store-structure-rowkey-design.md deleted file mode 100644 index af8b8aa..0000000 --- a/source/_drafts/hbase-store-structure-rowkey-design.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -layout: drafts -title: HBase 存储结构和 RowKey 设计 -date: 2017-04-04 22:09:10 -comments: true -ads: true -categories: [大数据,HBase] -tags: [大数据,HBase] ---- - -我所在的项目是一个电信网管系统,主要业务是基于电信基站上报话务单据来统计各种指标,来帮助电信运营商了解网络状况并指导网优工作。前两年,系统由原来基于 Akka Actor 的分布式计算系统迁移到了 Spark + Hadoop 所构建的大数据平台。虽然系统在水平扩展性,数据容量方面有了很大提升,但在稳定性和实时性上仍然饱受客户诟病。今年首要的重点工作就是补齐系统这两方面的能力。 - -## 现状 - -系统内部用 Spark 做网络相关统计指标的计算,并给客户呈现报表。报表是由相应的指标度量加上业务类型,客户,网关等多个维度组成。对于整个系统来说,一个比较大的问题是维度太多,可能的维度组合也更加多。现在每增加一个报表就需要新增代码,同时汇总任务也需要新增,不同的汇总任务之间容易出现重复运算不说也给系统的扩展性带来了灾难。 - -// TODO 贴上流程图 diff --git a/source/_drafts/make-mine-blogs-2.md b/source/_drafts/make-mine-blogs-2.md deleted file mode 100644 index 942428a..0000000 --- a/source/_drafts/make-mine-blogs-2.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: drafts -title: Allen's Blogs 创建历程(2) -date: 2016-09-18 22:09:10 -comments: true -ads: true -categories: 随便乱写 -tags: [Blogs,Jekyll,软件工程师的自我宣传] ---- - -## 从 Jekyll 到 Hexo - -玩了一段时间的 Jekyll 后,发现其还是有一些不尽如人意的地方。也许正是因为“轻量级”,Jekyll 似乎并不能满足我*不折腾*的要求。接着我开始考察 Hexo,和Jekyll相比,选择Hexo主要原因是: - -+ Jeky基于Ruby实现,安装Jeky需要搭建Ruby环境,在Windows搭建Ruby环境并不是被推荐的,而 Hexo基于NodeJs实现,在Windows上安装NodeJs开发环境简单。 -+ Jekyll没有本地服务器,无法实现本地博文预览功能,需要上传到WEB容器中才能预览功能,而Hexo可以通过简单的命令实现本地的预览,并直接发布到WEB容器中实现同步。 -+ 比较直接的另一个原因是在网上查找了很多博客的主题,发现Jekyll官网提供的主题都不怎么好看(可能是个人原因),而Hexo的主题看的比较顺眼。 -+ 两者都支持Markdown语法,这点我非常喜欢。 -+ Hexo 有内置的 Deployer,可以很方便的部署到不同的站点上(为什么需要这个功能?到后面讲针对百度的 SEO 时,我会说到)。 diff --git a/source/_drafts/spark-task-life-cycle.md b/source/_drafts/spark-task-life-cycle.md deleted file mode 100644 index 269fdac..0000000 --- a/source/_drafts/spark-task-life-cycle.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -layout: drafts -title: Spark 任务的生命周期 -date: 2016-09-18 22:09:10 -comments: true -ads: true -categories: [大数据,Spark] -tags: [Spark,Scala,大数据] ---- - -在日常的工作中,相信诸位小伙伴已经把 RDD 的 API 玩得相当熟练了,也了解了 transform 和 action 的区别。那你知道当你触发一个 action 后,在 Spark 内部究竟发生了什么么?接下来让我们打开 Spark 的源码揭示一个 Spark 任务内部的奥秘。 - -## 一些概念 -在开始之前我们先来回顾一些 Spark 的概念: - -- Jobs 是提交到调度器(scheduler)的顶层工作项(the top-level work items)。举个例子,当用户调用诸如`count()`这样的 action 方法时,Spark 会通过`submitJob`方法将这个 Job 提交到调度器。每个 Job 可能需要执行多个 Stage 来构建它的过程数据。 - -- Stages 是 Job 中计算中间结果的任务集,其中每个任务会在 RDD 的每个分区上运行相同的函数。Stages 是以 RDD 上的 transform 是否需要进行 Shuffle 来进行划分的, - -* -* - Jobs (represented by [[ActiveJob]]) are the top-level work items submitted to the scheduler. -* For example, when the user calls an action, like count(), a job will be submitted through -* submitJob. Each Job may require the execution of multiple stages to build intermediate data. -* -* - Stages ([[Stage]]) are sets of tasks that compute intermediate results in jobs, where each -* task computes the same function on partitions of the same RDD. Stages are separated at shuffle -* boundaries, which introduce a barrier (where we must wait for the previous stage to finish to -* fetch outputs). There are two types of stages: [[ResultStage]], for the final stage that -* executes an action, and [[ShuffleMapStage]], which writes map output files for a shuffle. -* Stages are often shared across multiple jobs, if these jobs reuse the same RDDs. -* -* - Tasks are individual units of work, each sent to one machine. -* -* - Cache tracking: the DAGScheduler figures out which RDDs are cached to avoid recomputing them -* and likewise remembers which shuffle map stages have already produced output files to avoid -* redoing the map side of a shuffle. -* -* - Preferred locations: the DAGScheduler also computes where to run each task in a stage based -* on the preferred locations of its underlying RDDs, or the location of cached or shuffle data. -* -* - Cleanup: all data structures are cleared when the running jobs that depend on them finish, -* to prevent memory leaks in a long-running application. diff --git a/source/_drafts/spark-task-serialize-detail.md b/source/_drafts/spark-task-serialize-detail.md deleted file mode 100644 index ce99e73..0000000 --- a/source/_drafts/spark-task-serialize-detail.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: drafts -title: Spark Task 序列化详解 -date: 2016-09-18 22:09:10 -comments: true -ads: true -categories: [大数据,Spark] -tags: [Spark,Scala,序列化] ---- diff --git a/source/_posts/2010-06-20-graduate.md b/source/_posts/2010-06-20-graduate.md deleted file mode 100644 index 1b7c560..0000000 --- a/source/_posts/2010-06-20-graduate.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -layout: post -title: "我的毕业综合症" -date: 2010-06-20 13:50:39 -comments: true -ads: true -categories: 随便乱写 -tags: - - 吐槽 - - 毕业 ---- - -最近听到的消息有点多, - -FOR EXAMPLE, - -XX获得 UNIVERSITY XXX 的 OFFER (XXX可以替代 Cambridge,Oxford,OR Stanford etc..)。 - - - -OR - -XX的PHD获批,一年XXXXX欧元研究经费,还不用交学费, - -OR - -XX雅思6+、7+,GRE 1400+…… - -P.S:暂时没有听见XX被Microsoft, Google, Ericsson, IBM, Oracle录取,不然我彻底崩溃 - -然后我就开始质疑我的选择是否正确,并纠结着是否给Dan发个邮件问他能不能赏个PHD我读读,最后开始感叹同样是人,怎么差距就那么大。 - -我开始担心三年后,我的激情是否会被相对死板的中国银行给浇灭,然后成为一个完全没有自我意识软件流水线操作员。 - -我开始担心三年后,我不在对代码感兴趣,而是对办公室里的权力斗争感兴趣,特别是在中国银行这样一个非创新性企业。 - -好吧,我承认我开始俗了,我开始担心是否有车有房,是否能为和我一同来到这个世界上的小兄弟找个港湾。 - -接着想到民工般的工资,毒贩般的工作压力,GFW,Evil的Party & Gov,悲剧的中国软件业~~~ - -开始觉得有点渺茫~~ - -Jason在考试成绩公布后,在Google Talk上留下一句极其霸气的话: - -Undergraduate is done, now its time to change the world. - -好吧,我承认我比一个35+,并有家室,小孩的人还悲观~~ - -最后膜拜下Jason - - diff --git a/source/_posts/2016-08-19-resolve-java-app-ssl-error.md b/source/_posts/2016-08-19-resolve-java-app-ssl-error.md deleted file mode 100644 index f2a7d79..0000000 --- a/source/_posts/2016-08-19-resolve-java-app-ssl-error.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -layout: post -title: "在公司内网如何更新IntelliJ的插件" -date: 2016-08-19 22:21:39 -comments: true -ads: true -categories: 软件技术 -tags: [Java, IntelliJ,ssl] ---- - -最近小伙伴们更新IntelliJ后,发现没法安装或者更新插件了,每次尝试在线安装时总会提示SSL错误。特别是要玩Scala的小伙伴更是抓狂,因为本身IntelliJ并不自带Scala的支持,需要下载Scala插件。不得以,只能通过手动下载,但是这样就不能享受插件更新的新功能了,很是不爽。那么报SSL错误的原因是什么呢?其实是因为IntelliJ更新插件时使用了Https连接,在连接时,客户端和服务器是要相互校验证书的,一般来说,只要证书正确,客户端是可以和服务器正常交互的。但是,我们是在公司内网,用的是公司的Proxy连接外网。公司的代理服务器会将证书换成公司自己颁(wei)发(zao)的证书(满满的[中间人攻击](https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB)的即视感,公司这样做是要干嘛?你懂的。。。),这时IntelliJ就无法同插件服务器正常通信了,那么怎么解决这个问题呢?那就是导入公司代理服务器的根证书,把公司颁(wei)发(zao)的证书变成可信任的证书。 - - - -OK, Let's do it! 首先导出公司代理服务器的根证书,用浏览器即可,随便访问应该https的外网网站,点击地址栏上的小锁头。 - - - -打开的窗口中,点击下一步即可, - - - -在正式编码格式中,选择指定的格式,点击下一步; - - - -指定生成证书文件的名称(此处为vbooking.cer) - - - -接着,将证书导入java的cacerts证书库,切换到目录 ${JAVA_HOME}/jre/lib/security, 执行如下命令 - -``` -keytool -import -alias vbooking -keystore cacerts -file ${cert_file_path} -``` - -其中: - -* -alias 指定别名(推荐和证书同名) -* -keystore 指定存储文件(此处固定) -* -file 指定证书文件全路径(证书文件所在的目录) - -此时命令行会提示你输入cacerts证书库的密码,敲入changeit即可,这是java中cacerts证书库的默认密码,当然也可自行修改。 - -最后,在系统中新建一个环境变量,IDEA_JDK(64位程序为IDEA_JDK_64),指向刚才导入根证书的JDK,不然IntelliJ会使用内置的JDK(详细见[这里](https://intellij-support.jetbrains.com/hc/en-us/articles/206544879-Selecting-the-JDK-version-the-IDE-will-run-under)),重启IntelliJ后即可。 diff --git a/source/_posts/2016-08-24-how-to-use-github.md b/source/_posts/2016-08-24-how-to-use-github.md deleted file mode 100644 index 4ac587c..0000000 --- a/source/_posts/2016-08-24-how-to-use-github.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -layout: post -title: "Github的正确使用方法" -date: 2016-08-24 16:21:39 -comments: true -ads: true -categories: 软件技术 -tags: [Git,Github] ---- - -在了解了Git的基本用法后(如果你还未了解 Git 的基本使用方法,建议你先话点时间阅读下《 Pro Git 》这本书),相信你已经开始跃跃欲试了,那么我就说下如何正确的使用 Github。下面的图描述了使用 Github 的基本流程: - - - - - -### 第一步:Fork项目 -Fork 项目其实就是在 Github 上拷贝一份他人项目的副本作为自己的项目。当你进入一个项目页面后,会在右上方看见一个*Fork*的按钮,点击它就可以 Fork 一个项目。 - - - -需要注意的是Fork项目后,你自己的项目并不会和源项目保持自动同步,所以你需要手动进行更新,如何更新请看:*第五步:拉取源项目的更新*。 - -### 第二步:Clone 到本地 -Fork 项目后,我们就可以把代码 Clone 到本地以便我们修改。Github 提供两种 Clone 项目的方式,SSH/HTTPS。如果选用SSH模式,你需要先在本地生成一对SSH Key并上传到Github用于身份识别,具体请参考 Github 的帮助文档:[Generating an SSH key](https://help.github.com/articles/generating-an-ssh-key/)。如果选用HTTPS模式,在更新和提交时就要输入 Github 的用户名和密码。一般来说使用 SSH 模式,在一次配置后,就可以免输密码提交代码,比较方便,但使用 HTTPS 模式更具备通用性,所以各有利弊,随意选择~ - -``` -# 使用 ssh clone 项目到本地 -$ git clone git@github.com:rvm/rvm.git - -# 使用 https clone 项目到本地 -$ git clone https://github.com/rvm/rvm.git -``` - -### 第三步:创建分支 - -每次开发新功能,都应该新建一个单独的分支(这方面可以参考[《Git分支管理策略》](http://www.ruanyifeng.com/blog/2012/07/git.html))。 - -``` -# 获取主干最新代码 -$ git checkout master -$ git pull - -# 新建一个开发分支myfeature -$ git checkout -b myfeature - -``` - -### 第四步:Commit 新代码 -分支修改后,就可以提交commit了。 - -``` -$ git add --all -$ git status -$ git commit --verbose -``` -- git add 命令的all参数,表示保存所有变化(包括新建、修改和删除)。从Git 2.0开始,all是 git add 的默认参数,所以也可以用 git add . 代替。 -- git status 命令,用来查看发生变动的文件。 -- git commit 命令的verbose参数,会列出 diff 的结果。 - -需要注意的是 Commit 代码必须给出简明扼要的提交信息,下面是一个范本,第一行是不超过50个字的提要,然后空一行,罗列出改动原因、主要变动、以及需要注意的问题。最后,提供对应的网址(比如Bug ticket)。 - -``` -Present-tense summary under 50 characters - -* More information about commit (under 72 characters). -* More information about commit (under 72 characters). - -http://project.management-system.com/ticket/123 -``` - -### 第五步:拉取源项目的更新 -当我们在修改代码的时候,源项目肯定也会发生变化,所以在我们向源项目推送代码之前,需要先将源项目的代码更新拉取下来。 - -先查看我们的 Remote 配置 - -``` -$ git remote -v -origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch) -origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push) -``` - -将源项目添加为 upstream - -``` -$ git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git -``` - -检查配置是否生效 - -``` -$ git remote -v -origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch) -origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push) -upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (fetch) -upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (push) -``` - -拉取源项目的变更 - -``` -git fetch upstream -remote: Counting objects: 75, done. -remote: Compressing objects: 100% (53/53), done. -remote: Total 62 (delta 27), reused 44 (delta 9) -Unpacking objects: 100% (62/62), done. -From https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY - * [new branch] master -> upstream/master -``` - -切换到 master 分支 - -``` -$ git checkout master -``` - -将源项目的修改合并到本地 master 分支 - -``` -git merge upstream/master -``` - -### 第六步:Rebase 本地分支并解决冲突 -接着我们切换到之前的开发分支 myfeature,并同 master 分支进行同步 - -``` -$ git checkout myfeature -$ git rebase master -``` - -有时我们会和主干发生冲突,那么我们需要在本地把所有冲突解决掉后才能继续合入代码。如何解决冲突,请阅:[Resolving a merge conflict from the command line](https://help.github.com/articles/resolving-a-merge-conflict-from-the-command-line/) - -### 第七步:Push到Github -同步好本地分支后,我们就可以将代码推送到Github了 - -``` -git push -u origin myfeature -``` - -### 第八步:发送Pull Request -点击项目页面上方的pull request按钮 - - - -我们自己的项目选择之前的开发分支,源项目选择 master 分支 - - - -在下面的页面上填写上描述,然后点击发送即可,接着下来就是原作者的事儿了,如果他同意合入我们会在项目的 master 分支看到我们刚刚贡献的代码。 diff --git a/source/_posts/2016-10-20-edgemax-ss-tutorial.md b/source/_posts/2016-10-20-edgemax-ss-tutorial.md deleted file mode 100644 index f99a385..0000000 --- a/source/_posts/2016-10-20-edgemax-ss-tutorial.md +++ /dev/null @@ -1,383 +0,0 @@ ---- -layout: post -title: "使用 EdgeMax 路由器自动翻墙" -date: 2016-10-20 22:21:39 -comments: true -ads: true -categories: [网络技术] -tags: [GFW,防火墙,EdgeMax,路由器] ---- - -作为肉身在墙内的计算机科学技术人员,不能上google实在是一件很遗憾的事情。 - - > 什么?你说用百度?你站出来,我是你老板我肯定会开除你。 - - 百度搜索简直是垃圾中的战斗机,在使用百度时,你不得不忍受他的各种广告,各种竞价排名,而且英文资料极少。很多领先的开源作品、解决方案、论文什么的基本都是国外的。这些资料你查不到,你说什么与国际先进技术接轨?总之,没用过 Google 之前,你可能没什么感觉,但是用过了之后再用"某度”,你会抱怨,这搜的是些什么破玩意儿。。。但是,因为众所周知的原因,我们无法直接访问 Google,不能访问一些很优秀的国外网站,比如 slideshare(里面有很多优秀的PPT、文档)等等。那么怎么办?答案就是:翻墙! - - - - > 网监同志,我知道你在盯着我,我写这个纯粹是为了方便技术人员查阅资料,作为爱党爱国的四有青年,我翻墙出去后保证不受反动思想的荼毒,努力在墙外为祖国占领舆论高地 - - 翻墙的姿势有很多,什么 VPN,代理,自由门等等,本人也尝试过不少,目前来说用得最稳定的当属[Shadowsocks](https://shadowsocks.org)。刚开始的时候只是在一个 Shadowsocks 服务提供商处购买了一个账号,在我的 Macbook 上试用,后面发现翻墙速度快且稳定,就萌生了在路由器上安装 Shadowsocks 的想法,正好家中的主路由器是 EdgeRouter Lite 3(Unix 架构,完美!),于是:Let's do it! - -## 概述 -在搜索引擎中输入`Shadowsocks+路由器`关键字,可以搜索出很多安装教程,采用的方案也不尽相同,建议不想太折腾的话就买一个可以刷 OpenWRT 的路由器,按照这个博客([https://cokebar.info/archives/978](https://cokebar.info/archives/978))的教程安装配置就可以了。如果你和我一样入了 EdgeRouter 的坑,那我们继续~。 - -我采用的方案是使用 Shadowsocks + ChinaDNS + DNSMasq + iptables 来实现路由器智能翻墙,即国内流量走正常网络,国外流量走 Shadowsocks 代理,总体流程如下图: - - - -## 所需软件 - -### shadowsocks-libev - -项目地址:[https://github.com/shadowsocks/shadowsocks-libev](https://github.com/shadowsocks/shadowsocks-libev),可下载源码在路由器上进行编译,或者直接下载[安装包](https://www.onlyos.com/wp-content/uploads/2015/08/shadowsocks-libev_2.2.4-1_mips.zip),请注意这个安装包只适用于 EdgeRouter Lite 3,其他 EdgeMax 产品需要自行编译。 - -### ChinaDNS - -项目地址:[https://github.com/shadowsocks/ChinaDNS](https://github.com/shadowsocks/ChinaDNS),可下载源码在路由器上进行编译,或者直接下载[安装包](https://www.onlyos.com/wp-content/uploads/2015/08/chinadns-1.3.2.zip),请注意这个安装包只适用于 EdgeRouter Lite 3,其他 EdgeMax 产品需要自行编译。 - -### DNSMasq -EdgeMax 中已经集成了 DNSMasq 无需另外安装。 - -## 配置 - -### shadowsocks-libev - -shadowsocks-libev 安装好后,会在`/etc/init.d/`中安装一个启动脚本:`shadowsock-libev`,在路由器启动时会默认启动`ss-redir`服务,如果需要重启 Shadowsocks,可以使用命令:`sudo /etc/init.d/shadowsock-libev [start|stop|restart]`。Shadowsocks 的配置在文件`/etc/shadowsocks-libev/config.json`中,在这个文件中配置 Shadowsocks 需要链接的服务器信息: - -``` -{ -"server":"127.0.0.1", -"server_port":8388, -"local_address": "0.0.0.0", -"local_port":1080, -"password":"barfoo!", -"timeout":60, -"method":null -} -``` - -配置完成后,`sudo /etc/init.d/shadowsock-libev restart`,这样 Shadowsocks 就在你的路由器上运行了。 - -### ChinaDNS - -使用源码编译后,会生成一个二进制文件:chinadns,可以将这个文件复制到`/usr/bin`中方便后面使用。ChinaDNS 需要一个文件来标识哪些 IP 属于国内,这个文件可以从[这里下载](http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest): - -``` -# 下载 ip 列表到: /tmp/chnroute.txt -curl 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep ipv4 | grep CN | awk -F| ‘{ printf(“%s/%dn”, $4, 32-log($5)/log(2)) }’ > /tmp/chnroute.txt -``` - -下载完成后,将文件移动到`/etc/chinadns/chnroute.txt`,接着我们就可以尝试启动 ChinaDNS 了: - -``` -chinadns -s 223.5.5.5,223.6.6.6,127.0.0.1:5300 -c /etc/chinadns/chnroute.txt -p 35353 -m -``` - -需要注意的是`-s`参数后面的`127.0.0.1:5300`,这个代表将`ss-tunnel`转发的国外可信DNS站点作为上游DNS服务器。 - -#### ChinaDNS参数详解 - -+ -l:虚假IP列表:默认值:/etc/chinadns_iplist.txt - -是GFW常见的DNS污染用IP列表,解析出列表中的IP结果时候,ChinaDNS会自动抛弃,保留默认即可; - -+ -c:chnroute文件:默认值:/etc/chinadns_chnroute.txt - -此文件标识哪些IP属于国内。用于ChinaDNS判断解析结果。ChinaDNS要求解析结果与DNS要匹配,国内网站采用国内DNS解析的结果,国外网站采用国外DNS解析结果,等等规则;确保以上两个文件内容完整无误,否则会造成无法启动; - -+ -p:本地端口:默认值:5353 - -ChinaDNS所监听的端口。根据实际情况更改,注意不能和其他服务的端口重复(特别是DNSMasq和shadowsocks); - -+ -s: 上游服务器:默认值:114.114.114.114,8.8.8.8 - -可填入一系列的上游DNS服务器,根据实际情况来,可以保留默认,格式为”DNS_IP:PORT,DNS_IP:PORT”注意逗号后面不能有空格。有些ISP会封杀公共DNS,此时请将114DNS改为ISP的DNS;此处必须至少填入一个国内IP的DNS和一个国外IP的DNS,否则会造成ChinaDNS启动失败。额外的用法:ChinaDNS添加可信DNS避免一些异常 - -+ -y:等待时间: 默认值:0.3 - -为防止GFW的DNS污染抢答,而设置一个等待时间,请根据自己填写的国外DNS延迟值来填写,留下一定的裕度。GoogleDNS在国内延迟一般在100-200ms,留0.3比较合适。过大的值会造成DNS解析较大的延迟时间,过小的值可能导致无法接收正确的解析结果。 - -+ -d:双向过滤: 默认:开启 - -勾选时,当国外DNS服务器返回的查询结果是国内IP,或者当国内DNS服务器返回的查询结果是国外IP,则过滤掉这个结果(较为严格的模式);去掉勾选的话只是过滤国内DNS的国外IP结果。 - -+ -m:启用压缩指针: 默认:不开启 - -利用GFW遇到压缩指针时的一个bug来精确识别来自GFW的抢答污染,从而极大提高识别的准确性和识别的效率,推荐启用,启用后,IPList和等待时间将禁用(因为用不到了)。 (已强制开启) - -### ss-tunnel - -ss-tunnel 是 Shadowsocks 的一个模块,可以用于 UDP 转发,为了防止 GFW 的 DNS 污染,我们用它来转发国外 DNS,可以通过下面命令来启动 ss-tunnel: - -``` -ss-tunnel -c /etc/shadowsocks-libev/config.json -u -b 0.0.0.0 -l 5300 -L 8.8.8.8:53 -``` -为了让 ss-tunnel 在每次重启路由器的时候自动重启,我们可以写个脚本放在`/config/scripts/post-config.d`目录下。 - -### DNSMasq - -首先需要将系统的 DNS 服务器设置为本地 - -``` -configure -# 停止通过pppoe更新dns设置,如果pppoe绑定在其他网口上,eth0需要变更为对应网口 -set interfaces ethernet eth0 pppoe 0 name-server none - -# 设置 DNSMasq 使用 ChinaDNS -# 需要注意的是,这里无法设置服务器端口,只能先这样设置后,再手工变更配置文件 -edit service dns forwarding -set name-server 127.0.0.1 - -# 告诉路由器使用本地 DNSMasq 来解析域名 -set system name-server 127.0.0.1 - -commit -save -exit -``` - -修改`/etc/dnsmasq.conf`,去除你之前自定义的规则,在最后加入`conf-dir=/etc/dnsmasq.d`,并将`server=127.0.0.1`修改为`server=127.0.0.1#35353`。新建并进入目录`/etc/dnsmasq.d`,下载 [accelerated-domains.china.conf](https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf) 和[foreign_list.conf](http://pan.baidu.com/s/1eQB7ACi)两个文件后复制两个文件到`/etc/dnsmasq.d`目录。这两个文件都会有更新,建议隔段时间更新一下。分别修改两个文件,将`accelerated-domains.china.conf`(ChinaList)文件中所有的的114.114.114.114修改为自己ISP的DNS或者其他效果更好的国内DNS的IP地址(也可以保留114DNS),格式为:` -server=/0-6.com/IP`;将`foreign_list.conf`(GFWList)文件中所有的 127.0.0.1#5300 修改为自己所用国外DNS,格式为:`server=/.lsxszzg.com/IP#PORT`,如果你使用shadowsocks的UDP转发来提供国外DNS解析,UDP转发的端口号为5300,那么就是默认的 127.0.0.1#5300 ,如果你使用一个非标端口的国外DNS服务,如3.4.5.6,端口5353,那么就改为 3.4.5.6#5353 。注意不要使用国外公共DNS,因为会被污染!最后重启 dnsmasq: - -``` -sudo /etc/init.d/dnsmasq restart -``` - -### iptables -上面所做的一切都是为了防止 GFW 的 DNS 污染,从而拿到正确的 IP 地址,那么拿到 IP 地址后,又如何将国外的流量转发到 Shadowsocks 呢?现在该 iptables 登场了,使用下面的代码设置 iptables 转发规则: - -``` - # Setup the ipset - ipset -N chnroute hash:net maxelem 65536 - - for ip in $(cat '/etc/chinadns/chnroute.txt'); do - ipset add chnroute $ip - done - - # 其他请求: - # shadowsocks - iptables -t nat -N SHADOWSOCKS - - iptables -t nat -A SHADOWSOCKS -p tcp --dport 23596 -j RETURN - # 23596 是 ss 代理服务器的端口,即远程 shadowsocks 服务器提供服务的端口,如果你有多个 ip 可用,但端口一致,就设置这个 - iptables -t nat -A SHADOWSOCKS -d 123.456.789.111 -j RETURN - # 123.456.789.111 是 ss 代理服务器的 ip, 如果你只有一个 ss服务器的 ip,却能选择不同端口,就设置此条 - - # ignore internal ip - iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN - - # ignore asia ip - - # Allow connection to chinese IPs - iptables -t nat -A SHADOWSOCKS -p tcp -m set --match-set chnroute dst -j RETURN - - iptables -t nat -A SHADOWSOCKS -p tcp -j REDIRECT --to-ports 1080 - iptables -t nat -I PREROUTING -p tcp -j SHADOWSOCKS - -``` - -iptables 配置完后,你应该可以再浏览器中正常的打开 Google 首页了。 - -### 一键式 DNS 配置脚本 -使用下面的脚本可以在重启路由器时(需要将脚本放在`/config/scripts/post-config.d`目录下)更新`foreign_list.conf`和`accelerated-domains.china.conf`两个文件,并配置好 iptables: - -``` -#!/bin/sh - -deleteFile() { - local filePath=$1 - if [ -f "$filePath" ]; then - rm "$filePath" - fi -} -# get chinadns ignore list -updateChnroute() { - deleteFile /tmp/chnroute.txt - - curl 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest' | grep ipv4 | grep CN | awk -F\| '{ printf("%s/%d\n", $4, 32-log($5)/log(2)) }' > /tmp/chnroute.txt - mv -f /tmp/chnroute.txt /etc/chinadns/ - chmod 644 /etc/chinadns/chnroute.txt - - if pidof chinadns>/dev/null; then - /etc/init.d/chinadns restart - fi -} - -updateDnsmasqConf() { - deleteFile /tmp/accelerated-domains.china.conf - # download accelerated-domains.china.conf - local DNS=223.5.5.5 - curl 'https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf' > /tmp/accelerated-domains.china.conf - sed -i "s|^\(server.*\)/[^/]*$|\1/$DNS|" /tmp/accelerated-domains.china.conf - mv -f /tmp/accelerated-domains.china.conf /etc/dnsmasq.d/ - chmod 644 /etc/dnsmasq.d/accelerated-domains.china.conf - - # download foreign_list.conf - python ../gfwlist2dnsmasq_noipset.py - - if pidof dnsmasq>/dev/null; then - /etc/init.d/dnsmasq restart - fi -} - - -configIptables() { - # Setup the ipset - ipset -N chnroute hash:net maxelem 65536 - - for ip in $(cat '/etc/chinadns/chnroute.txt'); do - ipset add chnroute $ip - done - - # 其他请求: - # shadowsocks - iptables -t nat -N SHADOWSOCKS - - iptables -t nat -A SHADOWSOCKS -p tcp --dport 33348 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 103.192.224.122 -j RETURN - - # ignore internal ip - iptables -t nat -A SHADOWSOCKS -d 0.0.0.0/8 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 10.0.0.0/8 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 127.0.0.0/8 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 169.254.0.0/16 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 172.16.0.0/12 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 192.168.0.0/16 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 224.0.0.0/4 -j RETURN - iptables -t nat -A SHADOWSOCKS -d 240.0.0.0/4 -j RETURN - - # ignore asia ip - - # Allow connection to chinese IPs - iptables -t nat -A SHADOWSOCKS -p tcp -m set --match-set chnroute dst -j RETURN - - iptables -t nat -A SHADOWSOCKS -p tcp -j REDIRECT --to-ports 1080 - iptables -t nat -I PREROUTING -p tcp -j SHADOWSOCKS -} - -updateChnroute -updateDnsmasqConf -configIptables -``` - -gfwlist2dnsmasq_noipset.py: - -``` -#!/usr/bin/env python -#coding=utf-8 -# -# Generate a list of dnsmasq rules with ipset for gfwlist -# -# Copyright (C) 2014 http://www.shuyz.com -# Ref https://code.google.com/p/autoproxy-gfwlist/wiki/Rules - -import urllib2 -import re -import os -import datetime -import base64 -import shutil -import ssl - -mydnsip = '127.0.0.1' -mydnsport = '5300' -# Extra Domain; -EX_DOMAIN=[ \ -'.google.com', \ -'.google.com.hk', \ -'.google.com.tw', \ -'.google.com.sg', \ -'.google.co.jp', \ -'.google.co.kr', \ -'.blogspot.com', \ -'.blogspot.sg', \ -'.blogspot.hk', \ -'.blogspot.jp', \ -'.blogspot.kr', \ -'.gvt1.com', \ -'.gvt2.com', \ -'.gvt3.com', \ -'.1e100.net', \ -'.blogspot.tw' \ -] - -# the url of gfwlist -baseurl = 'https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt' -# match comments/title/whitelist/ip address -comment_pattern = '^\!|\[|^@@|^\d+\.\d+\.\d+\.\d+' -domain_pattern = '([\w\-\_]+\.[\w\.\-\_]+)[\/\*]*' -ip_pattern = re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b') -tmpfile = '/tmp/gfwlisttmp' -# do not write to router internal flash directly -outfile = '/tmp/dnsmasq_list.conf' -rulesfile = '/etc/dnsmasq.d/foreign_list.conf' - -fs = file(outfile, 'w') -fs.write('# gfw list ipset rules for dnsmasq\n') -fs.write('# updated on ' + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + '\n') -fs.write('#\n') - -print 'fetching list...' -if hasattr(ssl, '_create_unverified_context'): - ssl._create_default_https_context = ssl._create_unverified_context -content = urllib2.urlopen(baseurl, timeout=15).read().decode('base64') - -# write the decoded content to file then read line by line -tfs = open(tmpfile, 'w') -tfs.write(content) -tfs.close() -tfs = open(tmpfile, 'r') - -print 'page content fetched, analysis...' - -# remember all blocked domains, in case of duplicate records -domainlist = [] - - -for line in tfs.readlines(): - if re.findall(comment_pattern, line): - print 'this is a comment line: ' + line - #fs.write('#' + line) - else: - domain = re.findall(domain_pattern, line) - if domain: - try: - found = domainlist.index(domain[0]) - print domain[0] + ' exists.' - except ValueError: - if ip_pattern.match(domain[0]): - print 'skipping ip: ' + domain[0] - continue - print 'saving ' + domain[0] - domainlist.append(domain[0]) - fs.write('server=/.%s/%s#%s\n'%(domain[0],mydnsip,mydnsport)) - else: - print 'no valid domain in this line: ' + line - -tfs.close() - -for each in EX_DOMAIN: - fs.write('server=/%s/%s#%s\n'%(each,mydnsip,mydnsport)) - -print 'write extra domain done' - -fs.close(); -print 'moving generated file to dnsmasg directory' -shutil.move(outfile, rulesfile) - -print 'done!' - -``` - -## 一键式安装包 - -现在风声紧,一键安装就不提供了。 diff --git a/source/_posts/2016-10-20-learn-gfw.md b/source/_posts/2016-10-20-learn-gfw.md deleted file mode 100644 index c830142..0000000 --- a/source/_posts/2016-10-20-learn-gfw.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -layout: post -title: "全面学习GFW" -date: 2016-10-20 16:21:39 -comments: true -ads: true -categories: 网络技术 -tags: [GFW,防火墙,研究] ---- - -摘自:[翻墙路由器的实现与原理](https://docs.google.com/document/d/1mmMiMYbviMxJ-DhTyIGdK7OOg581LSD1CZV4XY1OMG8/edit#heading=h.qgojh5xsppyz) - -注:本文只供研究学习网络技术使用,请阅读者在24小时内忘记本文内容,谢绝网监请喝茶。 - -GFW会是一个长期的存在。要学会与之共存,必须先了解GFW是什么。做为局外人,学习GFW有六个角度。渐进的来看分别是: - -首先我们学习到的是WHAT和WHEN。比如说,你经常听到人的议论是“昨天”,“github”被封了。其中的昨天就是WHEN,github就是WHAT。这是学习GFW的最天然,最朴素的角度。在这个方面做得非常极致的是一个叫做[greatfire](https://en.greatfire.org/)的网站。这个网站长期监控成千上万个网站和关键词。通过长期监控,不但可以掌握WHAT被封锁了,还可以知道WHEN被封的,WHEN被解封的。 - - - -接下来的角度是WHO。比如说,“方校长”这个人名就经常和GFW同时出现。但是如果仅仅是掌握一个两个人名,然后像某位同志那样天天在twitter上骂一遍那样,除了把这个人名骂成名人之外,没有什么特别的积极意义。我更看好这篇文章“通过分析论文挖掘防火长城(GFW)的技术人员”的思路。通过网络上的公开信息,掌握GFW的哪些方面与哪些人有关系,这些合作者之间又有什么联系。除了大家猜测的将来可以鞭尸之外,对现在也是有积极的意义的。比如关注这些人的研究动态和思想发展,可以猜测GFW的下一步发展方向。比如阅读过去发表的论文,可以了解GFW的技术演进历史,可以从历史中找到一些技术或者管理体制上的缺陷。 - -再接下来就是WHY了。github被封之后就常听人说,github这样的技术网站你封它干啥?是什么原因促成了一个网站的被封与解封的?我们做为局外人,真正的原因当然是无从得知的。但是我们可以猜测。基于猜测,可以把不同网站被封,与网络上的舆情时间做关联和分类。我们知道,方校长对于网路舆情监控是有很深入研究的。有一篇[论文](https://docs.google.com/file/d/0B3q8z0KnC5z6Zjc4VzdfOVN3RzA/edit?usp=sharing)(Whiskey, Weed, and Wukan on the World Wide Web: On Measuring Censors’ Resources and Motivations)专门讨论监管者的动机的。观测触发被封的事件与实际被封之间的时间关系,也可以推测出一些有趣的现象。比如有人报告,OpenVPN触发的封端口和封IP这样的事情一般都发生在中国的白天。也就是说,GFW背后不光是机器,有一些组件是血肉构成的。 - -剩下的两个角度就是对如何翻墙穿墙最有价值的两个角度了:HOW和WHERE。HOW是非常好理解的,就是在服务器和客户端两边抓包,看看一个正常的网络通信,GFW做为中间人,分别给两端在什么时候发了什么包或者过滤掉了什么包。而这些GFW做的动作,无论是过滤还是发伪包又是如何干扰客户端与服务器之间的正常通信的。WHERE是在知道了HOW之后的进一步发展,不但要了解客户端与服务器这两端的情况,更要了解GFW是挂在两端中间的哪一级路由器上做干扰的。在了解到GFW的关联路由器的IP的基础上,可以根据不同的干扰行为,不同的运营商归属做分组,进一步了解GFW的整体部署情况。 - -整体上来说,对GFW的研究都是从WHAT和WHEN开始,让偏人文的就去研究WHO和WHY,像我们这样偏工程的就会去研究HOW和WHERE。以上就是全面了解GFW的主体脉络。接下来,我们就要以HOW和WHERE这两个角度去看一看GFW的原理。 - -# GFW的原理 -要与GFW对抗不能仅仅停留在什么不能访问了,什么可以访问之类的表面现象上。知道youtube不能访问了,对于翻墙来说并无帮助。但是知道GFW是如何让我们不能访问youtube的,则对下一步的翻墙方案的选择和实施具有重大意义。所以在讨论如何翻之前,先要深入原理了解GFW是如何封的。 - -总的来说,GFW是一个分布式的入侵检测系统,并不是一个严格意义上的防火墙。不是说每个出入国境的IP包都需要先经过GFW的首可。做为一个入侵检测系统,GFW把你每一次访问facebook都看做一次入侵,然后在检测到入侵之后采取应对措施,也就是常见的连接重置。整个过程一般话来说就是: - - - -检测有两种方式。一种是人工检测,一种是机器检测。你去国新办网站举报,就是参与了人工检测。在人工检测到不和谐的网站之后,就会采取一些应对方式来防止国内的网民访问该网站。对于这类的封锁,规避检测就不是技术问题了,只能从GFW采取的应对方式上采取反制措施。另外一类检测是机器检测,其检测过程又可以再进一步细分: - - - -## 重建 -重建是指GFW从网络上监听过往的IP包,然后分析其中的TCP协议,最后重建出一个完整的字节流。分析是在这个重建的字节流上分析具体的应用协议,比如HTTP协议。然后在应用协议中查找是不是有不和谐的内容,然后决定采用何种应对方式。 - -所以,GFW机器检测的第一步就是重建出一个字节流。那么GFW是如何拿到原始的IP包的呢?真正的GFW部署方式,外人根本无从得知。据猜测,GFW是部署在国家的出口路由器的旁路上,用“分光”的方式把IP包复制一份到另外一根光纤上,从而拿到所有进出国境的IP包。下图引在gfwrev.blogspot.com: - - - -但是Google在北京有自己的机房。所以聪明的网友就使用Google的北京机房提供的GAE服务,用Goagent软件达到高速翻墙的目的。但是有网友证实([https://twitter.com/chengr28/status/260970749190365184](https://twitter.com/chengr28/status/260970749190365184)),即便是北京的机房也会被骨干网丢包。事实上Google在北京的谷翔机房有一个独立的AS(BGP的概念)。这个AS与谷歌总部有一条IPV6的直连线路,所以通过这个机房可以用IPV6不受墙的限制出去。但是这个AS无论是连接国内还是国外都是要经过GFW的。所以机房在北京也不能保证国内访问不被墙。GFW通过配置骨干网的BGP路由规则,是可以让国内的机房也经过它的。另外一个例子是当我们访问被封的网站触发连接重置的时候,往往收到两个RST包,但是TTL不同。还有一个例子是对于被封的IP,访问的IP包还没有到达国际出口就已经被丢弃。所以GFW应该在其他地方也部署有设备,据推测是在省级骨干路由的位置。 - -对于GFW到底在哪这个话题,最近又有国外友人表达了兴趣([https://github.com/mothran/mongol](https://github.com/mothran/mongol))。笔者在前人的基础上写了一个更完备的[探测工具](https://github.com/fqrouter/qiang)。其原理是基于一个IP协议的特性叫TTL。TTL是Time to Live的简写。IP包在没经过一次路由的时候,路由器都会把IP包的TTL减去1。如果TTL到零了,路由器就不会再把IP包发给下一级路由。然后我们知道GFW会在监听到不和谐的IP包之后发回RST包来重置TCP连接。那么通过设置不同的TTL就可以知道从你的电脑,到GFW之间经过了几个路由器。比如说TTL设置成9不触发RST,但是10就触发RST,那么到GFW就是经过了10个路由器。另外一个IP协议的特性是当TTL耗尽的时候,路由器应该发回一个TTL EXCEEDED的ICMP包,并把自己的IP地址设置成SRC(来源)。结合这两点,就可以探测出IP包是到了IP地址为什么的路由器之后才被GFW检测到。有了IP地址之后,再结合IP地址地理位置的数据库就可以知道其地理位置。据说,得出的位置大概是这样的: - - - -但是这里检测出来的IP到底是GFW的还是骨干路由器的?更有可能的是骨干路由器的IP。GFW做为一个设备用“分光”的方式挂在主干路由器旁边做入侵检测。无论如何,GFW通过某种神奇的方式,可以拿到你和国外服务器之间来往的所有的IP包,这点是肯定的。更严谨的理论研究有:[Internet Censorship in China: Where Does the Filtering Occur?](http://pam2011.gatech.edu/papers/pam2011--Xu.pdf) - -GFW在拥有了这些IP包之后,要做一个艰难的决定,那就是到底要不要让你和服务器之间的通信继续下去。GFW不能太过于激进,毕竟全国性的不能访问国外的网站是违反GFW自身存在价值的。GFW就需要在理解了IP包背后代表的含义之后,再来决定是不是可以安全的阻断你和国外服务器之间的连接。这种理解就要建立了前面说的“重建”这一步的基础上。大概用图表达一下重建是在怎么一回事: - - - -重建需要做的事情就是把IP包1中的GET /inde和IP包2中的x.html H和IP包3中的TTP/1.1拼到一起变成GET /index.html HTTP/1.1。拼出来的数据可能是纯文本的,也可能是二进制加密的协议内容。具体是什么是你和服务器之间约定好的。GFW做为窃听者需要猜测才知道你们俩之间的交谈内容。对于HTTP协议就非常容易猜测了,因为HTTP的协议是标准化的,而且是未加密的。所以GFW可以在重建之后很容易的知道,你使用了HTTP协议,访问的是什么网站。 - -重建这样的字节流有一个难点是如何处理巨大的流量?这个问题在这篇[博客](http://gfwrev.blogspot.tw/2010/02/gfw.html)中已经讲得很明白了。其原理与网站的负载均衡器一样。对于给定的来源和目标,使用一个HASH算法取得一个节点值,然后把所有符合这个来源和目标的流量都往这个节点发。所以在一个节点上就可以重建一个TCP会话的单向字节流。 - -最后为了讨论完整,再提两点。虽然GFW的重建发生在旁路上是基于分光来实现的,但并不代表整个GFW的所有设备都在旁路。后面会提到有一些GFW应对形式必须是把一些GFW的设备部署在了主干路由上,比如对Google的HTTPS的间歇性丢包,也就是GFW是要参与部分IP的路由工作的。另外一点是,重建是单向的TCP流,也就是GFW根本不在乎双向的对话内容,它只根据监听到的一个方向的内容然后做判断。但是监听本身是双向的,也就是无论是从国内发到国外,还是从国外发到国内,都会被重建然后加以分析。所以一个TCP连接对于GFW来说会被重建成两个字节流。具体的证据会在后面谈如何直穿GFW中详细讲解。 - -## 分析 -分析是GFW在重建出字节流之后要做的第二步。对于重建来说,GFW主要处理IP协议,以及上一层的TCP和UDP协议就可以了。但是对于分析来说,GFW就需要理解各种各样的应用层的稀奇古怪的协议了。甚至,我们也可以自己发明新的协议。 - -总的来说,GFW做协议分析有两个相似,但是不同的目的。第一个目的是防止不和谐内容的传播,比如说使用Google搜索了“不该”搜索的关键字。第二个目的是防止使用翻墙工具绕过GFW的审查。下面列举一些已知的GFW能够处理的协议。 - -对于GFW具体是怎么达到目的一,也就是防止不和谐内容传播的就牵涉到对HTTP协议和DNS协议等几个协议的明文审查。大体的做法是这样的。 - - - -像HTTP这样的协议会有非常明显的特征供检测,所以第一步就没什么好说的了。当GFW发现了包是HTTP的包之后就会按照HTTP的协议规则拆包。这个拆包过程是GFW按照它对于协议的理解来做的。比如说,从HTTP的GET请求中取得请求的URL。然后GFW拿到这个请求的URL去与关键字做匹配,比如查找Twitter是否在请求的URL中。为什么有拆包这个过程?首先,拆包之后可以更精确的打击,防止误杀。另外可能预先做拆包,比全文匹配更节省资源。其次,xiaoxia和liruqi同学的[jjproxy](https://github.com/liruqi/jjproxy)的核心就是基于GFW的一个HTTP拆包的漏洞,当然这个bug已经被修复了。其原理就是GFW在拆解HTTP包的时候没有处理有多出来的rn这样的情况,但是你访问的google.com却可以正确处理额外的rn的情况。从这个例子中可以证明,GFW还是先去理解协议,然后才做关键字匹配的。关键字匹配应该就是使用了一些高效的正则表达式算法,没有什么可以讨论的。 - -HTTP代理和SOCKS代理,这两种明文的代理都可以被GFW识别。之前笔者认为GFW可以在识别到HTTP代理和SOCKS代理之后,再拆解其内部的HTTP协议的正文。也就是做两次拆包。但是分析发现,HTTP代理的关键字列表和HTTP的关键字列表是不一样的,所以笔者现在认为HTTP代理协议和SOCKS代理协议是当作单独的协议来处理的,并不是拆出载荷的HTTP请求再进行分析的。 - -目前已知的GFW会做的协议分析如下: - -### DNS 查询 -GFW可以分析53端口的UDP协议的DNS查询。如果查询的域名匹配关键字则会被DNS劫持。可以肯定的是,这个匹配过程使用的是类似正则的机制,而不仅仅是一个黑名单,因为子域名实在太多了。证据是:2012年11月9日下午3点半开始,防火长城对Google的泛域名 .google.com 进行了大面积的污染,所有以 .google.com 结尾的域名均遭到污染而解析错误不能正常访问,其中甚至包括不存在的域名(来源:[http://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E5%8A%AB%E6%8C%81](http://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E5%8A%AB%E6%8C%81)) - -目前为止53端口之外的查询也没有被劫持。但是TCP的DNS查询已经可以被TCP RST切断了,表明了GFW具有这样的能力,只是不屑于大规模部署。而且TCP查询的关键字比UDP劫持的域名要少的多。目前只有dl.dropbox.com会触发TCP RST。相关的研究论文有: - -+ [Hold-On: Protecting Against On-Path DNS Poisoning](http://corelabs.coresecurity.com/index.php?module=Wiki&action=attachment&type=researcher&page=Ezequiel_Gutesman&file=publication/Predicting_attacker_tools_risk_assessment/gutesman-abstract-4pgs.pdf) - -+ [The Great DNS Wall of China](http://cs.nyu.edu/~pcw216/work/nds/final.pdf) - -### HTTP 请求 -GFW可以识别出HTTP协议,并且检查GET的URL与HOST。如果匹配了关键字则会触发TCP RST阻断。前面提到了jjproxy使用的构造特殊的HTTP GET请求欺骗GFW的做法已经失效,现在GFW只要看到rn就直接TCP RST阻断了(来源u/0/108661470402896863593/posts/6U6Q492M3yY)。相关的研究论文有: - -+ [The Great Firewall Revealed](http://www.internetfreedom.org/files/WhitePaper/ChinaGreatFirewallRevealed.pdf) - -+ [Ignoring the Great Firewall of China](http://www.cl.cam.ac.uk/~rnc1/ignoring.pdf) - -+ [HTTP URL/深度关键字检测](http://gfwrev.blogspot.com/2010/03/http-url.html) - -+ [ConceptDoppler: A Weather Tracker for Internet Censorship](http://www.csd.uoc.gr/~hy558/papers/conceptdoppler.pdf) - -### HTTP 响应 -GFW除了会分析上行的HTTP GET请求,对于HTTP返回的内容也会做全文关键字检查。这种检查与对请求的关键字检查不是由同一设备完成的,而且对GFW的资源消耗也更大。相关的研究论文有: - -+ [Empirical Study of a National-Scale Distributed Intrusion Detection System: Backbone-Level Filtering of HTML Responses in China](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.191.206&rep=rep1&type=pdf) - -### HTTP代理协议 -TODO - -### SOCKS4/5代理协议 -TODO - -### SMTP 协议 -因为有很多翻墙软件都是以邮件索取下载地址的方式发布的,所以GFW有针对性的封锁了SMTP协议,阻止这样的邮件往来。 - -封锁有三种表现方式([http://fqrouter.tumblr.com/post/43400982633/gfw-smtp](http://fqrouter.tumblr.com/post/43400982633/gfw-smtp)),简单概要的说就是看邮件是不是发往上了黑名单的邮件地址的(比如xiazai@upup.info就是一个上了黑名单的邮件地址),如果发现了就立马用TCP RST包切断连接。 - -### 电驴(ed2k)协议 -GFW还会过滤电驴(ed2k)协议中的查询内容。因为ed2k还有一个混淆模式,会加密往来的数据包,GFW会切断所有使用混淆模式的ed2k连接,迫使客户端使用明文与服务器通讯([http://fqrouter.tumblr.com/post/43490772120/gfw-ed2k](http://fqrouter.tumblr.com/post/43490772120/gfw-ed2k))。然后如果客户端发起了搜索请求,查找的关键字中包含敏感词的话就会被用TCP RST包切断连接。 - -### 对翻墙流量的分析识别 -GFW的第二个目的是封杀翻墙软件。为了达到这个目的GFW采取的手段更加暴力。原因简单,对于HTTP协议的封杀如果做不好会影响互联网的正常运作,GFW与互联网是共生的关系,它不会做威胁自己存在的事情。但是对于TOR这样的几乎纯粹是为翻墙而存在的协议,只要检测出来就是格杀勿论的了。GFW具体是如何封杀各种翻墙协议的,我也不是很清楚,事态仍然在不断更新中。但是举两个例子来证明GFW的高超技术。 - -第一个例子是GFW对TOR的自动封杀,体现了GFW尽最大努力去理解协议本身。根据这篇[博客](https://blog.torproject.org/blog/knock-knock-knockin-bridges-doors)。使用中国的IP去连接一个美国的TOR网桥,会被GFW发现。然后GFW回头(15分钟之后)会亲自假装成客户端,用TOR的协议去连接那个网桥。如果确认是TOR的网桥,则会封当时的那个端口。换了端口之后,可以用一段时间,然后又会被封。这表现出了GFW对于协议的高超检测能力,可以从国际出口的流量中敏锐地发现你连接的TOR网桥。据TOR的同志说是因为TOR协议中的握手过程具有太明显的特征了。另外一点就表现了GFW的不辞辛劳,居然会自己伪装成客户端过去连连看。 - -第二个例子表现了GFW根本不在乎加密的流量中的具体内容是不是有敏感词。只要疑似翻墙,特别是提供商业服务给多个翻墙,就会被封杀。根据这个[帖子](http://www.v2ex.com/t/55531),使用的ShadowSocks协议。预先部署密钥,没有明显的握手过程仍然被封。据说是GFW已经升级为能够机器识别出哪些加密的流量是疑似翻墙服务的。 - -关于GFW是如何识别与封锁翻墙服务器的,最近写了一篇文章提出我的猜想,大家可以去看看:[http://fqrouter.tumblr.com/post/45969604783/gfw](http://fqrouter.tumblr.com/post/45969604783/gfw)。 - -最近发现GFW对OpenVPN和SSL证书已经可以做到准实时的封IP(端口)。原理应该是离线做的深包分析,然后提取出可疑的IP列表,经过人工确认之后封IP。因为OpenVPN有显著的协议的特征,而且基本不用于商业场景所以很容易确认是翻墙服务。但是SSL也就是HTTPS用的加密协议也能基于“证书”做过滤不得不令人感到敬畏了。Shadowsocks的作者Clowwindy为此专门撰文:[为什么不应该用SSL翻墙](https://gist.github.com/clowwindy/5947691)。 - -总结起来就是,GFW已经基本上完成了目的一的所有工作。明文的协议从HTTP到SMTP都可以分析然后关键字检测,甚至电驴这样不是那么大众的协议GFW都去搞了。从原理上来说也没有什么好研究的,就是明文,拆包,关键字。GFW显然近期的工作重心在分析网络流量上,从中识别出哪些是翻墙的流量。这方面的研究还比较少,而且一个显著的特征是自己用没关系,大规模部署就容易出问题。我目前没有在GFW是如何封翻墙工具上有太多研究,只能是道听途说了。 - -# 应对 -GFW的应对措施是三步中最明显的,因为它最直接。GFW的重建过程和协议分析的过程需要耐心的试探才能大概推测出GFW是怎么实现的。但是GFW的应对手段我们每天都可以见到,比如连接重置。GFW的应对目前可以感受到的只有一个目的就是阻断。但是从广义上来说,应对方式应该不限于阻断。比如说记录下日志,然后做统计分析,秋后算账什么的也可以算是一种应对。就阻断方式而言,其实并不多,那么我们一个个来列举吧。 - -## 封IP -一般常见于人工检测之后的应对。还没有听说有什么方式可以直接使得GFW的机器检测直接封IP。一般常见的现象是GFW机器检测,然后用TCP RST重置来应对。过了一段时间才会被封IP,而且没有明显的时间规律。所以我的推测是,全局性的封IP应该是一种需要人工介入的。注意我强调了全局性的封IP,与之相对的是部分封IP,比如只对你访问那个IP封个3分钟,但是别人还是可以访问这样的。这是一种完全不同的封锁方式,虽然现象差不多,都是ping也ping不通。要观摩的话ping twitter.com就可以了,都封了好久了。 - -其实现方式是把无效的路由黑洞加入到主干路由器的路由表中,然后让这些主干网上的路由器去帮GFW把到指定IP的包给丢弃掉。路由器的路由表是动态更新的,使用的协议是BGP协议。GFW只需要维护一个被封的IP列表,然后用BGP协议广播出去就好了。然后国内主干网上的路由器都好像变成了GFW的一份子那样,成为了帮凶。 - - - -如果我们使用traceroute去检查这种被全局封锁的IP就可以发现,IP包还没有到GFW所在的国际出口就已经被电信或者联通的路由器给丢弃了。这就是BGP广播的作用了。 - -## DNS劫持 -这也是一种常见的人工检测之后的应对。人工发现一个不和谐网站,然后就把这个网站的域名给加到劫持列表中。其原理是基于DNS与IP协议的弱点,DNS与IP这两个协议都不验证服务器的权威性,而且DNS客户端会盲目地相信第一个收到的答案。所以你去查询facebook.com的话,GFW只要在正确的答案被返回之前抢答了,然后伪装成你查询的DNS服务器向你发错误的答案就可以了。 - - - -## TCP RST阻断 -TCP协议规定,只要看到RST包,连接立马被中断。从浏览器里来看就是连接已经被重置。我想对于这个错误大家都不陌生。据我个人观感,这种封锁方式是GFW目前的主要应对手段。大部分的RST是条件触发的,比如URL中包含某些关键字。目前享受这种待遇的网站就多得去了,著名的有facebook。还有一些网站,会被无条件RST。也就是针对特定的IP和端口,无论包的内容就会触发RST。比较著名的例子是https的wikipedia。GFW在TCP层的应对是利用了IPv4协议的弱点,也就是只要你在网络上,就假装成任何人发包。所以GFW可以很轻易地让你相信RST确实是Google发的,而让Google相信RST是你发的。 - - - -## 封端口 -GFW除了自身主体是挂在骨干路由器旁路上的入侵检测设备,利用分光技术从这个骨干路由器抓包下来做入侵检测 (所谓 IDS),除此之外这个路由器还会被用来封端口 (所谓 IPS)。GFW在检测到入侵之后可以不仅仅可以用TCP RST阻断当前这个连接,而且利用骨干路由器还可以对指定的IP或者端口进行从封端口到封IP,设置选择性丢包的各种封禁措施。可以理解为骨干路由器上具有了类似“iptables”的能力(网络层和传输层的实时拆包,匹配规则的能力)。这个iptables的能力在CISCO路由器上叫做ACL Based Forwarding (ABF)。而且规则的部署是全国同步的,一台路由器封了你的端口,全国的挂了GFW的骨干路由器都会封。一般这种封端口都是针对翻墙服务器的,如果检测到服务器是用SSH或者VPN等方式提供翻墙服务。GFW会在全国的出口骨干路由上部署这样的一条ACL规则,来封你这个服务器+端口的下行数据包。也就是如果包是从国外发向国内的,而且src(源ip)是被封的服务器ip,sport(源端口)是被封的端口,那么这个包就会被过滤掉。这样部署的规则的特点是,上行的数据包是可以被服务器收到的,而下行的数据包会被过滤掉。 - -如果被封端口之后服务器采取更换端口的应对措施,很快会再次被封。而且多次尝试之后会被封IP。初步推断是,封端口不是GFW的自动应对行为,而是采取黑名单加人工过滤地方式实现的。一个推断的理由就是网友报道,封端口都是发生在白天工作时间。 - -在进入了封端口阶段之后,还会有继发性的临时性封其他端口的现象,但是这些继发性的封锁具有明显的超时时间,触发了之后(触发条件不是非常明确)会立即被封锁,然后过了一段时间就自动解封。目前对于这一波封SSH/OPENVPN采用的以封端口为明显特征的封锁方式研究尚不深入。可以参考我最近写的一篇[文章](http://fqrouter.tumblr.com/post/45969604783/gfw) - - - -## HTTPS间歇性丢包 -对于Google的HTTPS服务,GFW不愿意让其完全不能访问。所以采取的办法是对于Google的某些IP的443端口采取间歇性丢包的措施。其原理应该类似于封端口,是在骨干路由器上做的丢包动作。但是触发条件并不只是看IP和端口,加上了时间间隔这样一个条件。 diff --git a/source/_posts/2016-10-26-spark-is-lazy.md b/source/_posts/2016-10-26-spark-is-lazy.md deleted file mode 100644 index 28f9d82..0000000 --- a/source/_posts/2016-10-26-spark-is-lazy.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -layout: post -title: "Spark 的惰性运算" -date: 2016-10-26 16:21:39 -comments: true -ads: true -categories: [大数据,Spark] -tags: [Spark,Scala,函数式编程,惰性计算] ---- - -今天在检视项目代码的时候,无意中发现了下面一段代码: - -```scala -class RddTransformer{ - - def doTransform(data: RDD[Data]): RDD[NewData]={ - val newDataRdd = data.flatmap(DataTransformer.doTransform) - - if(DataTransformer.exceptionCount > 0) { - logger.error(s"There are some illegal data, count: ${DataTransformer.exceptionCount}") - } - - newDataRdd - } -} - -object DataTransformer{ - var exceptionCount:Int = 0 - - def doTransform(data: Data): Option[NewData]={ - if(data.isIllegal){ - exceptionCount += 1 - None - }else{ - // do something transform data to new data - ..... - - Some(newData) - } - } -} -``` - - - -作者的意图很简单,就是将RDD中的数据转换为新的数据格式,并统计非法数据的个数。咋一看代码,似乎没有什么问题,可是,这段代码真的能得到正确的结果么?答案是否定的,事实上,不管RDD中包含多少非法数据,`if(DataTransformer.exceptionCount > 0)`这个条件永远都不会为真。为什么?你现在肯定充满了疑惑,让我们先来看看 Spark 的文档上对 RDD 操作的解释: - ->All transformations in Spark are lazy, in that they do not compute their results right away. Instead, they just remember the transformations applied to some base dataset (e.g. a file). The transformations are only computed when an action requires a result to be returned to the driver program. ([RDD Operations](http://spark.apache.org/docs/latest/programming-guide.html#rdd-operations)) - -> 在 Spark 中,所有的 transformation() 类型操作都是延迟计算的,Spark 只是记录了将要对数据集进行的操作。只有需要数据集将数据返回到 Driver 程序时(即触发 Action 类型操作),所有已记录的 transformation() 才会执行。 - -回到上面的代码,由于针对`RDD[Data]`的`flatmap`操作属于 transformation() 类型操作,所以`val newDataRdd = data.flatmap(DataTransformer.doTransform)`这段代码只是记录了一下对 RDD 的操作,并没有真正的去执行`DataTransformer.doTransform`方法中的代码。我们可以尝试在 Spark Shell 中实验一下: - -```shell -scala> var counter = 0 -counter: Int = 0 - -scala> var rdd = sc.parallelize(Seq(1,2,3,4,5,6)).map(x => counter += x) -rdd: spark.RDD[Int] = spark.MappedRDD@2ee9b6e3 - -scala> counter -counter: Int = 0 - -``` - -显然累加操作并没有被执行,根据 Shell 终端的输出,Spark 似乎只是记录了一下我们的操作,并返回了一个新的 RDD。当对 RDD 进行 transformation() 操作的时候,在 Spark 内部究竟发生了什么?在解释这个问题之前,先来看看 Spark 作业的执行逻辑。 - -## Spark Job 执行逻辑 - - - -典型的 Spark Job 逻辑执行图如下所示,Spark Job 经过下面四个步骤可以得到最终执行结果: - -+ 从数据源(可以是本地 file,内存数据结构, HDFS,HBase 等)读取数据创建最初的 RDD。上一段代码中的 parallelize() 相当于 createRDD()。 -+ 对 RDD 进行一系列的 transformation() 操作,每一个 transformation() 会产生一个或多个包含不同类型 T 的 RDD[T]。T 可以是 Scala 里面的基本类型或数据结构,不限于 (K, V)。但如果是 (K, V),K 不能是 Array 等复杂类型(因为难以在复杂类型上定义 partition 函数)。 -+ 对最后的 final RDD 进行 action() 操作,每个 partition 计算后产生结果 result。 -+ 将 result 回送到 driver 端,进行最后的 f(list[result]) 计算。例子中的 count() 实际包含了action() 和 sum() 两步计算。 - -Spark 在每次 transformation() 的时候使用了新产生的 RDD 来记录计算逻辑,这样就把作用在 RDD 上的所有计算逻辑串起来形成了一个链条,逻辑执行图上表示的实际上就是是 Spark Job 的计算链。当然某些 transformation() 比较复杂,会包含多个子 transformation(),因而会生成多个 RDD。这就是实际 RDD 个数会比我们想象的多一些的原因。当对 RDD 进行 action() 时,Spark 会调用在计算链条末端最后一个 RDD 的`compute()`方法,这个方法会接收它上一个 RDD 或者数据源的 input records,并执行自身定义的计算逻辑,从而输出结果。一句话总结 Spark 执行 action() 的流程就是:从计算链的最后一个 RDD 开始,依次从上一个 RDD 获取数据并执行计算逻辑,最后输出结果。 - -## 数据计算过程 - -下面的代码段,展现了`RDD.flatmap()`和`MapPartitionsRDD`的实现,在代码中,我们看到,当调用`RDD`的`map`并传入一个函数`f`的时候,Spark 并没有做什么运算,而是用`f`作为一个入参创建了一个叫`MapPartitionsRDD`的对象并返回给调用者。而在`MapPartitionsRDD.scala`中,我们也看到只有当`compute`方法被调用的时候,我们之前传入的函数`f`才会真正的被执行 - -```scala - - // RDD.scala - ... - /** - * Return a new RDD by applying a function to all elements of this RDD. - */ - def flatmap[U: ClassTag](f: T => U): RDD[U] = withScope { - val cleanF = sc.clean(f) - new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF)) - } - - // MapPartitionsRDD.scala - private[spark] class MapPartitionsRDD[U: ClassTag, T: ClassTag]( - var prev: RDD[T], - f: (TaskContext, Int, Iterator[T]) => Iterator[U], // (TaskContext, partition index, iterator) - preservesPartitioning: Boolean = false) - extends RDD[U](prev) { - - override val partitioner = if (preservesPartitioning) firstParent[T].partitioner else None - - override def getPartitions: Array[Partition] = firstParent[T].partitions - - override def compute(split: Partition, context: TaskContext): Iterator[U] = - f(context, split.index, firstParent[T].iterator(split, context)) - - override def clearDependencies() { - super.clearDependencies() - prev = null - } -} -``` - -实际计算过程大概是这样的: - -1. 根据动作操作来将一个应用程序划分成多个作业。 -2. 一个作业经历 DAG 调度和任务调度之后,被划分成一个一个的任务,对应 Task 类。 -3. 任务被分配到不同核心去执行,执行 Task.run。 -4. Task.run 会调用阶段末 RDD 的 iterator 方法,获取该 RDD 某个分区内的数据记录,而 iterator 方法有可能会调用 RDD 类的 compute 方法来负责父 RDD 与子 RDD 之间的计算逻辑。 - -整个过程会比较复杂,在此不进行展开,我们只需要知道 Apache Spark 最终会调用 RDD 的 iterator 和 compute 方法来计算分区数据即可。 - -### compute 方法 -在 RDD 中,`compute()`被定义为抽象方法,要求其所有子类都必须实现,该方法接受的参数之一是一个`Partition`对象,目的是计算该分区中的数据。以之前`flatmap`操作生成得到的`MapPartitionsRDD`类为例。 - -```scala -override def compute(split: Partition, context: TaskContext): Iterator[U] = - f(context, split.index, firstParent[T].iterator(split, context)) -``` - -其中,`firstParent`在 RDD 中定义。 - -```scala - /** Returns the first parent RDD */ - protected[spark] def firstParent[U: ClassTag] = { - dependencies.head.rdd.asInstanceOf[RDD[U]] - } -``` - -`MapPartitionsRDD`类的`compute`方法调用当前 RDD 内的第一个父 RDD 的`iterator`方法,该方的目的是拉取父 RDD 对应分区内的数据,它返回一个迭代器对象,迭代器内部存储的每个元素即父 RDD 对应分区内已经计算完毕的数据记录。得到的迭代器作为`f`方法的一个参数。`compute`方法会将迭代器中的记录一一输入`f`方法,得到的新迭代器即为所求分区中的数据。 - -### iterator方法 - -`iterator`方法的实现在 RDD 类中。 - -```scala - /** - * Internal method to this RDD; will read from cache if applicable, or otherwise compute it. - * This should ''not'' be called by users directly, but is available for implementors of custom - * subclasses of RDD. - */ - final def iterator(split: Partition, context: TaskContext): Iterator[T] = { - if (storageLevel != StorageLevel.NONE) { - SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel) - } else { - computeOrReadCheckpoint(split, context) - } - } -``` - -`iterator`方法首先检查当前 RDD 的存储级别,如果存储级别不为`None`,说明分区的数据要么已经存储在文件系统当中,要么当前 RDD 曾经执行过`cache`、`persise`等持久化操作,因此需要想办法把数据从存储介质中提取出来。`iterator`方法继续调用`CacheManager`的`getOrCompute`方法。 - -```scala - /** Gets or computes an RDD partition. Used by RDD.iterator() when an RDD is cached. */ - def getOrCompute[T]( - rdd: RDD[T], - partition: Partition, - context: TaskContext, - storageLevel: StorageLevel): Iterator[T] = { - val key = RDDBlockId(rdd.id, partition.index) - blockManager.get(key) match { - case Some(blockResult) => - // Partition is already materialized, so just return its values - context.taskMetrics.inputMetrics = Some(blockResult.inputMetrics) - new InterruptibleIterator(context, blockResult.data.asInstanceOf[Iterator[T]]) - case None => - // 省略部分源码 - val computedValues = rdd.computeOrReadCheckpoint(partition, context) - val cachedValues = putInBlockManager(key, computedValues, storageLevel, updatedBlocks) - new InterruptibleIterator(context, cachedValues) - } - // 省略部分源码 - } - ``` - -`getOrCompute`方法会根据 RDD 编号与分区编号计算得到当前分区在存储层对应的块编号,通过存储层提供的数据读取接口提取出块的数据。这时候会有两种可能情况发生: - -+ 数据之前已经存储在存储介质当中,可能是数据本身就在存储介质(如读取 HDFS 中的文件创建得到的 RDD)当中,也可能是 RDD 经过持久化操作并经历了一次计算过程。这时候就能成功提取得到数据并将其返回。 -+ 数据不在存储介质当中,可能是数据已经丢失,或者 RDD 经过持久化操作,但是是当前分区数据是第一次被计算,因此会出现拉取得到数据为 None 的情况。这就意味着我们需要计算分区数据,继续调用 RDD 类 computeOrReadCheckpoint 方法来计算数据,并将计算得到的数据缓存到存储介质中,下次就无需再重复计算。 -+ 如果当前RDD的存储级别为 None,说明为未经持久化的 RDD,需要重新计算 RDD 内的数据,这时候调用 RDD 类的 computeOrReadCheckpoint 方法,该方法也在持久化 RDD 的分区获取数据失败时被调用。 - -```scala - /** - * Compute an RDD partition or read it from a checkpoint if the RDD is checkpointing. - */ - private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] = { - if (isCheckpointed) firstParent[T].iterator(split, context) else compute(split, context) - } -``` - -`computeOrReadCheckpoint`方法会检查当前 RDD 是否已经被标记成检查点,如果未被标记成检查点,则执行自身的`compute`方法来计算分区数据,否则就直接拉取父 RDD 分区内的数据。 - -## 如何正确的获取计算结果 - -说了那么多理论,我们回到问题本身,怎么才是获取运算结果的正确方法?你也许会说,既然 transformation() 操作是惰性的,那么在之后马上触发一个 action() 操作就 OK 了。但这也是不正确的,这就涉及到了 Spark 的另外一个重要概念:分布式,在这里就不展开讲了,有兴趣可以参考官方文档:[Understanding closures ](http://spark.apache.org/docs/latest/programming-guide.html#understanding-closures-a-nameclosureslinka)。 - -下面是一个正确的实现: - -```scala - -class RddTransformer{ - - def doTransform(data: RDD[Data]): RDD[NewData]={ - val newDataRdd = data.flatmap(DataTransformer.doTransform).cache() - - val exceptionCount = newDataRdd.filter(_.isEmpty).count() - if(exceptionCount > 0) { - logger.error(s"There are some illegal data, count: ${DataTransformer.exceptionCount}") - } - - newDataRdd - } -} - -object DataTransformer{ - - def doTransform(data: Data): Option[NewData]={ - if(data.isIllegal){ - None - }else{ - // do something transform data to new data - ..... - - Some(newData) - } - } -} - -``` diff --git a/source/_posts/2016-12-13-fetchsize-jdbc-memory.md b/source/_posts/2016-12-13-fetchsize-jdbc-memory.md deleted file mode 100644 index b0e6830..0000000 --- a/source/_posts/2016-12-13-fetchsize-jdbc-memory.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -layout: post -title: Fetch Size 与 JDBC 内存管理 -date: 2016-12-13 16:21:39 -comments: true -ads: true -categories: [数据库] -tags: [JDBC,数据库,内存管理] ---- - -接触到 JDBC 的 Fetch Size 这个属性缘起一个性能问题,项目中需要将一个有千万级数据量的表中的记录导出到文件中去。按照正常的路数,先初始化连接;接着写好 SQL 语句,比如`SELECT * FROM DIM_USERS`;然后启动查询,拿到 ResultSet,最后遍历 ResultSet 将每行记录输出到文件中去。可在接下来的测试中,发现性能并不理想,在表中数据量小的时候,执行速度尚可接受,可是在进行大数据量压力测试的时候,发现代码往往要执行40分钟以上,这在实际生产环境上是万万不可接受的。 - - - -通过定位,发现性能瓶颈出现在从数据库中读取数据的时候,大概消耗了90%以上的时间。也就是说如果什么事情都不干,单纯对一个千万级数据量的 ResultSet 进行一次遍历就需要耗时35分钟以上。这样一来,问题就变得让人有点费解了,因为在同一套环境上,服务器向数据库写入数据的速率可以达到3万+/秒,为何查询变得如此低效?正在我百思不得其解的时候,一个大神走过来拍怕我的肩膀说:“小伙子,试试把 Fetch Size 调整一下。” - -## Fetch Size - -在 JDBC 中 Fetch Size 是 Statement 上的一个属性,先看下[Oracle 的帮助文档](https://docs.oracle.com/cd/E11882_01/java.112/e16548/resltset.htm#JJDBC28621)对它是怎么定义的: - ->By default, when Oracle JDBC executes a query, it receives the result set 10 rows at a time from the database cursor. This is the default Oracle row-prefetch value. You can change the number of rows retrieved with each trip to the database cursor by changing the row-prefetch value - -简单的说,这个属性控制了 JDBC 每次读取数据的行数,由于 JDBC 每次都要通过网络去读取数据,如果这个值配置得太小,那么就意味着在遍历 ResultSet 的时候 JDBC 需要频繁的通过网络读取数据,这就导致了读取数据时性能低下。那接下来的问题就简单了,就是将这个属性调大。可是调整到多少合适呢?1K、2K?还是1W、2W?要知道 JDBC 每次读取的数据是会缓存在内存中的,如果这个属性设置大了,就会使程序出现 OOM。 - -## JDBC Memory -接下来就得聊聊 JDBC 的内存管理了(这里特指 Oracle JDBC,别的厂商也许实现机制不是这样的)。JDBC 解析 SQL 语句后,为每个 Statement(包括 PreparedStatement 和 CallableStatement)分配了两个 Buffer 来缓存数据,`byte[]`和`char[]`。字符类型的数据(CHAR, -VARCHAR2, NCHAR, etc. )缓存在`char[]`中,其他类型的数据缓存在`byte[]`中。在 SQL 语句解析后,语句所查询的列的数据类型就已经确定了,JDBC 会根据这些信息和 Fetch Size 一起计算出缓存的大小,并分配内存。所以如果不需要查询某张表的所以列时,使用`SELECT * FROM XXX`是一种浪费内存的行为,特别是表的列数多且数据量大的时候,很容易造成 OOM。 - -### 数据类型与内存占用 -前面说了,JDBC 会根据查询语句中列的数据类型来计算缓存的大小那么每种数据类型大致占多少空间呢?请看下表。 - -| 数据类型 | 大小(byte) | 备注 | -| ------------- |:-------------:|: -----: | -| VARCHAR2 |2 |每个字符占用2byte | -| BFILE |4K | | -| BLOB | 4K | | -|CLOB |4K | | -|Other | 22 | 其他类型占用空间比较小,可以大致估算为22byte| - -让我们来举个栗子: - -``` -CREATE TABLE TAB (ID NUMBER(10), NAME VARCHAR2(40), DOB DATE) -ResultSet r = stmt.executeQuery(“SELECT * FROM TAB”); -``` - -当 JDBC 解析查询语句时,数据库会告知 JDBC 结果会包含三列,NUMBER(10)、VARCHAR2(40) 和 DATE,第一列大概需要22 bytes,第二列包含了40个字符,所以需要`2 * 40`bytes,第三列也是大概需要22 bytes。因此,本次查询每条记录大致需要`22 + (40 * 2) + 22 = 124`bytes,如果 Fetch Size设置为10,那么缓存就需要分配1240 bytes 的空间。 - -## 如何正确设置Fetch Size -上面说了那么多无非就是想说明一个问题,就是 Fetch Size 的大小是要根据实际情况来设置,设置小了性能不好,设置大了内存会有问题。总之一个原则就是,在保证内存够用的情况下,尽量把 Fetch Size 设置得大一点。如果你拿不准设置多少,可以先试下下面的方式: - -``` - 4 * 1024 * 1024 / sum(所读取的列的数据长度) -``` - -## 参考文档: - -+ [Oracle JDBC Memory Management](http://www.oracle.com/technetwork/database/enterprise-edition/memory.pdf) diff --git a/source/_posts/2017-04-30-new-home-network-structure.md b/source/_posts/2017-04-30-new-home-network-structure.md deleted file mode 100644 index e9caf44..0000000 --- a/source/_posts/2017-04-30-new-home-network-structure.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -layout: post -title: Ubnt UniFi 产品开箱 -date: 2017-04-30 16:21:39 -comments: true -ads: true -categories: [网络技术] -tags: [Ubnt,Wi-Fi,无线覆盖] ---- - -家里的无线网络覆盖一直有些问题,虽然说已经在家里部署了两个无线AP,但是还是一些小问题,首先信号覆盖还是有一些死角,比如说,卫生间,一进卫生间,信号强度瞬间掉到只有一格;其次就是两个 AP 之间相互协作好像有点问题,经常出现终端在 AP 1 的旁边,却连接到了 AP 2 上,只能手动断开 Wi-Fi,并重新连接。虽然说,这不是什么大问题,但对于一个有强迫症的 IT 男来说,这就像背痒一样,不挠一下不舒服。于是,我就打算把家里的无线网络改造一下。 - - - -# 一些背景和需求 - -先放一下家里的户型图,家虽然不算很大,正常来说,这样的户型靠一个普通的无线路由器就能完成全屋的覆盖。但是这套房子有点特殊,房间的墙不是普通的那种空心砖,全是钢筋混凝土浇筑的墙,不管路由器放在哪个房间,总有地方覆盖不到。 - - - -鉴于这种原因,就只能采用有线路由器加多个无线 AP 的网络结构,下面是我家网络原有的拓扑图。客厅和弱电箱之间,由于只拉了一条网线,而且我们家电视用的是电信的IPTV,所以只能通过划分不同的 VLAN 来达到一条网线跑两路数据的要求。其实大的网络结构上并没有什么问题,就是两个无线 AP 之间的协同有点让人闹心,所以这次网络改造的主要重点就是解决多个无线 AP 之间的协同问题。 - -# 器材选择 - -在这之前其实做过很多功课,传统的解决方案一般都是瘦 AP 加上 AC 控制器,但是这种方案并不适合于我。首先,市面上并没有太多面向家庭或者小企业的廉价 AC + 瘦 AP 解决方案,向华为、思科这种牌子,一个 AP 就要 2K 左右,AC 控制器更是天价;其次,弱电箱太小了,里面已经塞了一台光猫,一台路由器和一个交换机了,实在是再也塞不下一个 AC 控制器了。 - -前段时间 Ubnt 面向家用市场推出了一个 Wi-Fi Mesh 解决方案:Amplifi Wi-Fi。路由器和扩展点之间的连接走的是无线,省去了拉网线的烦恼,整个产品整体颜值也高,而且由于是全套解决方案,扩展点之间的协作就更加没有问题了。对于这个产品,我曾经有种草了很久,但是最近看到各路评测的开箱,发现这个产品根本不支持 VLAN 这就没法满足我看 IPTV 的需求了,而且扩展点和主路由之间的无线通讯肯定没有有线来的快,所以只能 PASS 这个产品。 - -后来我把目光放到了 Ubnt 的 UniFi 产品上,UniFi 的 AP 有个特点,就是可以用软 AC 进行控制,而且不要求 AC 实时在线。使用软 AC 配置好网络后,关闭 AC,整个网络依然可以正常工作,这样就省去了需要硬 AC的烦恼。而且现在家里用的路由器也是 Ubnt 的产品,本着凑一套的思路,我最终选择了 Ubnt UniFi。 - -# 开箱 - -通过万能的淘宝购买了三件 Ubnt 产品:UBNT US-8-60W 千兆 PoE 交换机、UBNT UAP-AC LITE 吸顶 PoE 无线 AP 和 UBNT UAP-AC-IW 入墙面板无线 AP,整套价格 2K 出头。好了,下面正式进入开箱环节。 - - - -第一个出场的是交换机,因为两个 AP 都需要 PoE 供电,所以一个具备 PoE 输出功能的交换机不可或缺。产品的包装盒十分简洁,没有什么多余的东西,盒子里面的附件除了机器本体外,还有一个 220V 的电源和一个简要的安装说明。 - - - -将交换机装入弱电箱,接上电源和路由器之间的网线。请忽略杂乱的布线,开发商装的弱电箱不是怎么给力,所以也就懒得去折腾了。 - - - -接下来是 UBNT UAP-AC LITE,产品包装同样很简洁,飞碟型的造型也很漂亮。家用的话买 LITE 也就足够了,尺寸比较小巧一点。 - - - -安装后的效果,别问我为什么吸顶天线不吸顶,因为天花板上没有预留网线,就那么简单。 - - - -最后一个出场的就是 UBNT UAP-AC-IW,它是 Ubnt 的一款新产品,以前 Ubnt 也有一款入墙式无线 AP,但是那货不支持国内的 86 盒,所以没法用。这个面板有三部分组成,安装底座、AP 本体和外盖板。 - - - - - -先将面板底座装到 86 盒上 - - - -接好线,盖上面板后的效果 - - - -一切安装就绪后,就可以用 UniFi Controller 来配置网络了。 - -# 配置网络 - -先到 Ubnt 官方网站将 UniFi Controller 软件下载好,并在电脑上安装。安装后,启动控制器,就可以开始配置网络了。刚开始需要配置时区信息。 - - - -然后下一页就能看见设备上线了。 - - - -设置好 Wi-Fi 信息 - - - -接下来设置好软件登陆信息就可以进入软件主界面了 - - - - - -# 总结 - -整套产品安装部署过程还是比较简单的,通过 UniFi Controller 可以集中管理网络中的 UniFi 设备,包括 AP 和交换机,省去了单独登陆各个设备单独进行配置的麻烦。两个 AP 在覆盖方面的表现也非常棒,房间中再也没有信号死角,而且设备在房间中移动也再也没有之前那种网络闪断的情况,这让我非常满意。 - - diff --git a/source/_posts/2017-05-25-Integer-highestOneBit.md b/source/_posts/2017-05-25-Integer-highestOneBit.md deleted file mode 100644 index c8c4826..0000000 --- a/source/_posts/2017-05-25-Integer-highestOneBit.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -layout: post -title: Integer的highestOneBit方法源码解析 -date: 2017-05-25 16:21:39 -comments: true -ads: true -categories: [软件技术] -tags: [Java,JDK 源码,Collection] ---- - -在读HashMap源码的时候,遇到了Integer的highestOneBit静态方法不是太理解,所以就读了一下源码,这里记录一下。 - -JDK的注释中说 - -> 返回具有至多單個 1 位的 int 值,在指定的 int 值中最高位(最左邊)的 1 位的位置。如果指定的值在其二進制補碼表示形式中不具有 1 位,即它等於零,則返回零。 - -用人话说 - -+ 如果一个数是0, 则返回0; -+ 如果是负数, 则返回 -2147483648:【1000,0000,0000,0000,0000,0000,0000,0000】(二進制表示的數); -+ 如果是正数, 返回的则是跟它最靠近的比它小的2的N次方 - - - -比如 17: - -二进制是【0000,0000,0000,0000,0000,0000,0001,0001】 - -highestOneBit(17)返回的是最高位的1个1, 其它全是0 的二进制數:【0000,0000,0000,0000,0000,0000,0001,0000】,其实就是16。 - -```java - /** - * 将一个整数(二进制)设置最高位为1,其它位为0,然后返回改变后的值 - * 如果这个整数是0返回0 - * @param i - * @return - */ - public static int highestOneBit(int i) { - // 例如1000 - i |= (i >> 1); // 使前2位变为1,相当于i = i | (i >> 1); i = 1000 | 0100 = 1100 - i |= (i >> 2); // 使前4位变为1,由于上一步确保了前两位都是1,所以这一次移动两位,1111 - i |= (i >> 4); // 使前8位变为1,1111 - i |= (i >> 8); // 使前16位变为1,1111 - i |= (i >> 16); // 使前32位变为1,1111 - return i - (i >>> 1); // i >>> 1 无符号右移,使最高位为0,其余位为1,相减即得出结果,1111 - 0111 = 1000 - } -``` - -1. ”|“是按位操作符的或操作符,按位操作符有与操作符(&),或操作符(|),异或操作符(^),取反操作符(~),其中运算规则如下: - - - &:两个输入位都为1才会输出1,否则输出0,如:10 & 11 -> 10 - - |:有一个输入位为1就会输出1,如:100 | 110 -> 110 - - ^:如果输入为的某一个是1,但不全都是1,那么输出1,如:110 ^ 100 -> 010 - - ~:这个就不用说了吧 - -2. ”>>>“是无符号右移操作符,移位操作符分为有符号左移位操作符(<<)、有符号右移位操作符(>>)、无符号右移操作符(>>>)。 -有符号左移时低位补0;有符号右移时,若符号为正则高位补0,反之补1;无符号右移操作符无论正负都在高位补0. diff --git a/source/_posts/2017-08-06-fix-sougou-intellij-compatibility.md b/source/_posts/2017-08-06-fix-sougou-intellij-compatibility.md deleted file mode 100644 index 70f1a82..0000000 --- a/source/_posts/2017-08-06-fix-sougou-intellij-compatibility.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: post -title: 解决在 IntelliJ IDEA 时,搜狗输入法不跟随问题 -date: 2017-08-06 16:21:39 -comments: true -ads: true -categories: [软件技术] -tags: [Java,IntelliJ,搜狗输入法] ---- - -最近从华为离职并入职了新的公司,在新领的电脑配置好开发环境后就开始愉快的打码。可是在我要输入中文注释的时候,发现在 IDE 里面没法正常使用搜狗输入法,表现为输入法候选框不跟随光标,输入后不弹出候选字。 - - - - - -其实候选框不跟随光标还好,但无法弹出候选字确实没法忍,总不能不写注释或者全部用英文写注释吧。这么干的话,后面的维护者一定会有想砍死我的想法。尝试了重装或者升级输入法,均没有解决。这个版本的 IDEA 之前也用过,也没有碰到这个输入法的问题,仔细想了下配置的差异,之前我喜欢把 IDEA 自身使用的 JDK 设置为系统中已经安装的那一个,而这次为了图省事就没指定,那会不会是这个原因导致的?果然,切换后问题解决。 - -关于如何设置 IDEA 的 JDK 的问题,Jetbrains 有一份[官方文档](https://intellij-support.jetbrains.com/hc/en-us/articles/206544879-Selecting-the-JDK-version-the-IDE-will-run-under)可以供大家参考,我给大家简要说明一下: - -+ 打开 IDEA 使用 `Help | Find Action`(可以使用快捷键 :Ctrl+Shift+A 或者 Cmd+Shift+A Mac平台) ,输入`Switch IDE Boot JDK`,按下回车; -+ `Switch IDE Boot JDK dialog` 对话框会弹出来,在对话框中选择系统安装的 JDK 或者直接输入 JDK 的路径 (比如 c:\Program Files (x86)\Java\jdk1.8.0_112 或者 /Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home/ or /usr/lib/jvm/open-jdk)。 -+ 点击 OK,并重启 IDEA 客户端,重启后打开 About IntelliJ IDEA 看看 JDK 是否设置成功,见下图: - - diff --git a/source/_posts/2017-09-03-static-synchronized-accident.md b/source/_posts/2017-09-03-static-synchronized-accident.md deleted file mode 100644 index a6c2b84..0000000 --- a/source/_posts/2017-09-03-static-synchronized-accident.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -layout: post -title: 一起 Static 和 Synchronized 引发的血案 -date: 2017-09-03 16:21:39 -comments: true -ads: true -categories: [软件技术] -tags: [Java,多线程] ---- - -这两天在定位一个网上问题的时候发现一个很诡异的现象,系统夜间的汇总任务跑了很长一段时间才能结束,而且日志显示这些汇总任务的每个子任务都很快就结束了,但整体任务还是耗费了很长一段时间才结束。 - -``` -sub-job 1 done in 3s -sub-job 2 done in 3s -sub-job 3 done in 2s -sub-job 4 done in 5s -sub-job 5 done in 6s -sub-job 6 done in 8s -sub-job 7 done in 9s -... -whole process is down in 3235s -``` - - - -其实整体业务流程很简单,大致的流程就是系统创建了很多汇总任务,把它们丢到线程池中去执行。这些任务在执行的过程中,为了提高效率,会创建一些子任务并并发的运行它们,当子任务运行结束后,父任务就会结束,所以出现这种现象是非常不科学的。我的第一感觉就是是不是任务间存在不合理的锁竞争导致线程相互等待?仔细检查代码,果然发现了问题,在汇总任务的父类中有这样一个方法: - -```java -private static synchronized format(DateTime dt){ - return "P" + dt.toString("yyyyMMHHmmss"); -} -``` - -这个方法是汇总任务根据时间生成目标汇总时间周期用的,之所以会封装成一个方法,估计是为了代码复用考虑。封装本身并没有错,但是要命的是,开发人员将方法声明为`static synchronized`,让我们先回忆一下这个两个关键字的作用: - -+ synchronized - - synchronized 关键字放在方法声明上时,表示该方法为`Synchronized Methods`,即同步方法,在[The Java™ Tutorials](https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html)中对同步方法有以下描述: - - >First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object. - - >Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads. - - 简单来说就是当一个方法声明为同步方法的时候,不可能出现多个线程同时调用同一个对象(注意是同一个对象,这点很重要)上的该方法,只有当一个线程调用结束,其他线程才有可能获取锁并执行该方法。 - -+ static - - 在Java中static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,当然也可以修饰代码块。被static修饰的成员变量和成员方法是独立于该类的,它不依赖于某个特定的实例变量,也就是说它被该类的所有实例共享。所有实例的引用都指向同一个地方,任何一个实例对其的修改都会导致其他实例的变化。 - -那么`synchronized`加上`static`会出现什么效果?按照上面的分析`static`是整个类共享的,不仅仅是一个对象,那么`static synchronized`修饰的变量、方法或者代码段就是在类的粒度上进行同步,而不是仅仅是在对象粒度上。对于这个问题,[Java machine language specification](http://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.3.6)中也有描述: - ->For a class (static) method, the monitor associated with the Class object for the method's class is used. - ->For an instance method, the monitor associated with this (the object for which the method was invoked) is used. - -所以在我们的业务代码中,如果在父类中声明了一个`static synchronized`的方法,就意味着每个继承它的子类及其对象在调用这个方法时都会争夺这个锁,那么造成任务执行效率低下也就是必然的了。 diff --git a/source/_posts/2018-05-11-win10_sleep.md b/source/_posts/2018-05-11-win10_sleep.md deleted file mode 100644 index a9fcb93..0000000 --- a/source/_posts/2018-05-11-win10_sleep.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -layout: post -title: 解决 Windows10 笔记本关上后仍会掉电的问题 -date: 2018-05-11 -comments: true -ads: true -categories: [操作系统] -tags: [Windows 10,睡眠,休眠] ---- - -一开始我只是想在谷歌上搜索一下,为啥我的 Win10 笔记本在盖上盖子以后放入背包中,每次拿出来之后电量都会有所下降,结果就一下子看到了很多很神奇的东西,包括 Win10 的几种节能状态,以及如何设置关闭盖子的行为。且听我一一道来。 - - - -## Win10 节能状态 - -总的来讲,自 Win7 到 Win8 到 Win10 以来,Windows 实际上一共有 3 种不同的节能状态,可参考该[链接](http://www.thewindowsclub.com/difference-between-sleep-hybrid-sleep-and-hibernation-in-windows-7)。链接中的文章很好地介绍了这 3 种状态,这里我就复制过来顺便简单翻译一下。 - -> **Sleep** is a power-saving state that allows a computer to quickly resume full-power operation (typically within several seconds) when you want to start working again. -> -> Putting your computer into the sleep state is like pausing a DVD player; the computer immediately stops what it ’ s doing and is ready to start again when you want to resume working. - -**睡眠**(Sleep)可以让计算机在你想要开始工作时迅速(通常在几秒钟内)恢复至全速运行状态。让你的计算机进入睡眠模式就像在 DVD 播放机上按下暂停按钮一样 —— 计算机会立刻停止它的当前任务并且随时准备好在你回来工作时再次启动。 - -> **Hibernation** is a power-saving state designed primarily for laptops. -> -> While sleep puts your work and settings in memory and draws a small amount of power, hibernation puts your open documents and programs on your hard disk and then turns off your computer. Of all the power-saving states in Windows, hibernation uses the least amount of power. On a laptop, use hibernation when you know that you won ’ t use your laptop for an extended period and won ’ t have an opportunity to charge the battery during that time. - -**休眠**(Hibernation)是一种主要为笔记本电脑设计的节能状态。睡眠实际上会把你当前的工作内容和设置放入到内存中,并且需要少量的电力来维持这些数据,而相比之下休眠则会把这些数据放入到磁盘中然后完全关闭你的计算机。在 Windows 中,休眠实际上是所需电力最少的节能状态。如果你使用的是笔记本电脑,那么如果你在一段较长的时间内都不会再使用你的电脑且这段时间也无法给它充电的话,你应该让它进入休眠状态。 - -> **Hybrid sleep** is designed primarily for desktop computers. Hybrid sleep is a combination of sleep and hibernate; it puts any open documents and programs in memory and on your hard disk and then puts your computer into a low-power state so that you can quickly resume your work. That way, if a power failure occurs, Windows can restore your work from your hard disk. When hybrid sleep is turned on, putting your computer into sleep automatically puts your computer into hybrid sleep. *Hybrid sleep is typically turned on by default on desktop computers and off by default on laptops*. - -**混合睡眠**(Hybrid Sleep)是一种主要为桌面电脑(台式机)设计的节能状态。混合睡眠实际上是睡眠与休眠的结合:它会把当前的工作内容继续保持在内存中,同时也把这些数据复制到磁盘中,然后再让你的计算机进入低耗能状态,如此一来你的计算机便可以快速地恢复当前状态,同时及时发生电力故障,Windows 仍然可以从磁盘中恢复当前的数据。在混合睡眠设置开启时,你令计算机进入睡眠模式时会让计算机自动进入混合睡眠模式。混合睡眠在桌面电脑上是默认开启的,而在笔记本电脑上则是默认关闭的。 - -## 设置 Win10 关闭盖子行为 - -“关闭盖子” 这个词听着有点怪,而部分 Win10 用户实际上也应该在电源选项中见过这个名字: - - - -实际上这个翻译也是挺奇怪的,而该选项的英文实际上是 “ Choose what closing the lid does ”,也就是配置电脑在盖上时应该做什么。 - -进入该页面,可看到设置如下: - - - -这里可以选择在接通与未接通电源两种情况下,按下电源按钮和关闭盖子时计算机应该采取的行为。选项包括 “不采取任何操作”、“睡眠”、“休眠”、“关机”,在了解过 “睡眠” 和 “休眠” 的差异后,我想这四个选项的含义就不难理解了。我们只要将**关闭盖子时**的行为设置为**休眠**即可。 - -实际上,“关闭盖子时”的设置默认为“睡眠”,而该模式仍需要一定的电力维持内存中的数据,而且睡眠状态下的笔记本极易被唤醒(被鼠标、键盘、定时事件等唤醒),因此这也就是为何大多数 Win10 笔记本经常在盖子关上时自行启动了。可见这并不是一个 Bug,而只是单纯的设置不当。 - -不过值得注意的是,休眠后的计算机无法在短时间内恢复。进入睡眠模式的计算机在恢复时往往能在瞬间进入登录界面,而进入休眠模式的计算机由于需要从磁盘中读取数据,往往需要经历一段和开机相当的时间。当然,如果你的计算机使用的是 SSD,这个不足则微乎其微。 - -从上一节中对睡眠和休眠的描述来看,你可以在确定自己比较长时间内不会使用计算机时才选择让计算机进入休眠,如果离开的时间较短则可以选择让其进入睡眠。因此,可以在上述设置中选择在按下电源按钮时让计算机进入睡眠状态,这样如果你只是要短时间离开计算机(上厕所、倒杯水等),就可以考虑按下电源按钮进入睡眠,而不是直接盖上计算机。 - -除此之外,在使用休眠模式时要尤其小心:休眠模式在恢复时需要把磁盘上的数据重新读入到内存,考虑到 Win10 的高兼容性,实际上这个过程很容易发生错误导致部分软件无法恢复到原本的状态继续运行,因此恢复时很容易导致部分驱动程序崩溃,令计算机发生所谓的“蓝屏”现象。休眠模式的不稳定实际上自 Win7 以来便一直存在,Win10 也无法幸免,有较小几率仍会“蓝屏”,但相比 Win7 已有很大的改善。我想,这算是我们选择了 Win10 相比于 Mac OS 高得多的兼容性后所必须承受的代价吧。 - -我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:[https://cloud.tencent.com/developer/support-plan?invite_code=36xfesel4yw4s](https://cloud.tencent.com/developer/support-plan?invite_code=36xfesel4yw4s) diff --git a/source/_posts/2018-12-31-charset-detect.md b/source/_posts/2018-12-31-charset-detect.md deleted file mode 100644 index 913ec1c..0000000 --- a/source/_posts/2018-12-31-charset-detect.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -layout: post -title: 字符编码的那些事 -date: 2018-12-30 16:21:39 -comments: true -ads: true -categories: [软件技术] -tags: [Java,字符编码] ---- - -最近在处理一个需求时发现业务字段出现了一串异常的字符。熟悉 web 开发的同学应该一眼就能看出,诸如`%C2%D6%BB%D8%CA%AF`之类的字符串是一个 URL Encoded 的字符串。导致这些未经 decode 的数据直接展示到界面上的原因是,业务日志中该字段格式不统一,有些是未经 URL Encoded 的,但有些又经过 URL Encoded 了,ETL 没有对这种情况进行处理就直接入库了。 - - - -为了解决这个问题,ETL 需要做的就是先要判断输入的字符串是否是 URL Encoded 的,如果不是,直接返回即可,如果不是则需要做进一步的处理 - -## 什么是 URL Encoding - -URL encoding是Uniform Resource Identifier(URI)规范文档中对特殊字符编码制定的规则。本质是把一个字符转为百分号(%)加上其字符编码对应的16进制数字。故又称之为Percent-encoding。一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号。比如,世界上有英文字母的网址"http://www.abc.com",但是没有希腊字母的网址"http://www.aβγ.com"(读作阿尔法-贝塔-伽玛.com)。所以如果 URL 中有中文,那么就必须进行 URL Encoding 。但是麻烦的是,相关规范并没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定,比如对于中文字符,应用程序可以先使用 UFT-8 编码后,再 URL encoding,也可以使用 GBK 编码后,再 URL encoding,这两种实现方式都是合法的,但最终产生的结果并不一样。这导致"URL编码"成为了一个混乱的领域(详细可以参考:[关于URL编码](http://www.ruanyifeng.com/blog/2010/02/url_encoding.html))。 - -## 字符编码识别 - -我们从数据中抽取了一些 URL encoded 数据并使用一些在线解析工具进行了解析,发现它们的原始编码是 GBK 的,由于担心数据中混有其他的编码格式,我们想如何自动的识别字符的编码格式。 - -### 使用`juniversalchardet`做字符编码识别 - -经过一番搜索,找到了一个叫`juniversalchardet`的工具。 它是Mozilla 公司的 firefox 使用的 universalchardet 编码自动检测工具的 Java 版本。自动编码主要是根据统计学的方法来判断。具体原理,可以看:[A composite approach to language/encoding detection](https://www-archive.mozilla.org/projects/intl/UniversalCharsetDetection.html) - -下面写个小例子来验证他的特性,首先使用 maven 引入依赖 - -```xml - -