Quantcast
Channel: IT瘾博客推荐
Viewing all 532 articles
Browse latest View live

博客启用InstantClick

$
0
0

博客» IT技术» »

之前我发现有些网站能瞬间显示新页面,几乎没有延迟,非常类似于AJAX的刷新,但页面URL的确又变了(如果是AJAX刷新,页面地址不会变)。现在我知道这可以用InstantClick实现。本着阅微堂一直在尝鲜最新技术的传统,我毫不犹豫就装上了。

InstantClick的官方主页见 http://instantclick.io/download。根据官方描述,它主要从两个方面加快页面加载速度:

  1. 网页浏览者在将鼠标挪到链接上(MouseOver)到点击鼠标(MouseDown)到点击完毕(MouseUp)一般都有400毫秒以上。不信的人可以到 http://instantclick.io/click-test这里测试一下自己的点击速度。浏览器一般在链接点击完毕后开始载入新页面。InstantClick则一旦发现鼠标挪到链接上就开始载入链接的页面内容,这可以让新页面的载入节约400毫秒以上。这个方法在手机浏览也有效。
  2. InstantClick使用pushState和Ajax技术,在显示新页面时并不是重新解释执行新页面,而只是替换页面的标题和页面内容。这样做有两个好处:其一是浏览器省去了重新解释代码和式样,让页面显示速度更快,尤其对于那些页面复杂和式样较多的页面;其二是浏览器可以无缝显示,不会在页面跳转中间闪一下白屏。

具体效果可以直接参考本博客。当网速较慢时,页面上方会显示载入进度条。

由于上面的第二个技术,这个插件只对使用同样式样文件的页面才有效,否则会出现白屏等情况。而且插件并不会自动识别,需要人工把这类链接加入黑名单。插件在这方面还可改进。

另外,该插件由于在原页面上直接更新内容,导致部分JavaScript会出问题。比如百度分享的代码必须在代码最前方加入以下代码才能正常工作:

window._bd_share_main


© 张志强 for 阅微堂, 2014. | 链接 | 0条评论


simple – 基于 Github 的极简博客系统

$
0
0

simple是简单的静态博客生成器,基于 Github Pages,静态页面,完全在线操作,不需要服务器,只需一个 Github 账号即可。

o

传统的独立博客玩法,需要域名、服务器、程序等等一系列服务才玩得转,当然功能也丰富的多。

simple需要 GitHub 账号,然后创建一个 username.github.io 的 project,注意要勾选生成 README。(支持绑定自己的域名)。

然后访问 http://isnowfy.github.io/simple/并使用 Github 账号登录,根据提示就可以完成设置并自动生成页面了。

最终的效果与作者的 介绍页面相同,详细阅读后就知道怎么做的,注意每次打开页面后记得点一次 New Post 再开始写作,URL 里记得加 .html,支持标签。

那么,你还在写博客么?

simple – 基于 Github 的极简博客系统,首发于 极客范 - GeekFan.net

黑客能利用不安全的cookies劫持你的WordPress博客

$
0
0
HTTPS Everywhere和Privacy Badger Firefox维护者Yan Zhu发现了WordPress的一个安全漏洞,该漏洞将允许黑客劫持你的WordPress博客。她发现一个重要的cookies“wordpress_logged_in”在输入有效的用户名和密码后通过HTTP明文发送到WordPress的一个认证端点。也就是说,你可以拷贝这个cookies到另一个浏览器内,然后在cookies有效期内直接登录到WordPress帐号,绕过了二步认证,不需要再输入用户名和密码。她测试后发现,这个cookies拥有发表文章阅读私人帖子和留言等诸多权限,甚至能修改帐号相关的电子邮件地址。WordPress首席开发者Andrew Nacin已经证实了这个漏洞,称将在下个WordPress 版本中修复该问题,指出漏洞不允许修改密码,因为修改密码需要使用到另一个认证cookies“wordpress_sec”。






hibernate优化总结(转自一博客)

$
0
0
1) 在处理大数据量时,会有大量的数据缓冲保存在Session的一级缓存中,这缓存大太时会严重显示性能,所以在使用Hibernate处理大数据量的,可以使用session.clear()或者session. Evict(Object) 在处理过程中,清除全部的缓存或者清除某个对象。
2) 对大数据量查询时,慎用list()或者iterator()返回查询结果,
1. 使用List()返回结果时,Hibernate会所有查询结果初始化为持久化对象,结果集较大时,会占用很多的处理时间。
2. 而使用iterator()返回结果时,在每次调用iterator.next()返回对象并使用对象时,Hibernate才调用查询将对应的对象初始化,对于大数据量时,每调用一次查询都会花费较多的时间。当结果集较大,但是含有较大量相同的数据,或者结果集不是全部都会使用时,使用iterator()才有优势。
3. 对于大数据量,使用qry.scroll()可以得到较好的处理速度以及性能。而且直接对结果集向前向后滚动。
3) 对于关联操作,Hibernate虽然可以表达复杂的数据关系,但请慎用,使数据关系较为简单时会得到较好的效率,特别是较深层次的关联时,性能会很差。
4) 对含有关联的PO(持久化对象)时,若default-cascade="all"或者 “save-update”,新增PO时,请注意对PO中的集合的赋值操作,因为有可能使得多执行一次update操作。
5) 在一对多、多对一的关系中,使用延迟加载机制,会使不少的对象在使用时方会初始化,这样可使得节省内存空间以及减少数据库的负荷,而且若PO中的集合没有被使用时,就可减少互数据库的交互从而减少处理时间。
6) 对于大数据量新增、修改、删除操作或者是对大数据量的查询,与数据库的交互次数是决定处理时间的最重要因素,减少交互的次数是提升效率的最好途径,所以在开发过程中,请将show_sql设置为true,深入了解Hibernate的处理过程,尝试不同的方式,可以使得效率提升。
7) Hibernate是以JDBC为基础,但是Hibernate是对JDBC的优化,其中使用Hibernate的缓冲机制会使性能提升,如使用二级缓存以及查询缓存,若命中率较高明,性能会是到大幅提升。
8) Hibernate可以通过设置hibernate.jdbc.fetch_size,hibernate.jdbc.batch_size等属性,对Hibernate进行优化。

9) 大数据量的处理,可以使用无状态Session,好处是对二级缓存的性能优化
  statelessSession=sessionFactory.openStatelessSession();
10) 对于大数据量新增、修改、删除操作,可以使用DML风格的HQL语句,好处是会直接绕过内存进行数据处理,JDBC风格,高效

注:以上转自一位博客的内容,仅作为工作参考用。

已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



使用 Flask 搭建静态博客

$
0
0

现在流行的静态博客/网站生成工具有很多,比如 Jekyll, Pelican, Middleman, Hyde 等等, StaticGen列出了目前最流行的一些静态网站生成工具。

我们的内部工具由 Python/Flask/MongoDB 搭建,现在需要加上文档功能,写作格式是 Markdown,不想把文档放到数据库里,也不想再弄一套静态博客工具来管理文档,于是找到了 Flask-FlatPages这个好用的 Flask 模块。熟悉 Flask 的同学花几分钟的时间就可以用搭建一个简单博客,加上 Bootstrap 的帮助,不到一小时内就可以用 Flask-Flatpages 弄个像模像样的网站出来。

创建开发环境

首先我们需要 pip,在 Mac 上最简单的安装办法是:

$ sudo easy_install pip
$ sudo easy_install virtualenv

如果你在 Mac 上用 Homebrew 包管理工具的话的话,也可以用 brew 升级 Python 和安装 pip:

$ brew update
$ brew install python

创建一个 blog 目录、生成 Python 独立虚拟环境并在这个环境里安装需要的 Flask, Flask-FlatPages 模块:

$ mkdir blog
$ cd blog

$ virtualenv flask
New python executable in flask/bin/python
Installing setuptools, pip...done.

$ flask/bin/pip install flask
$ flask/bin/pip install flask-flatpages

在 blog 目录下我们分别新建几个目录:static 用来存放 css/js 等文件,templates 用来存放 flask 要用的 Jinja2 模版,pages 用来存放我们静态博客(Markdown 格式):

$ mkdir -p app/static app/templates app/pages

程序

主程序 blog.py 的功能是,导入必要的模块、配置 Flask-FlatPages 模块需要的参数、创建 Flask 应用、写几个 URL 路由函数,最后运行这个应用:

$ vi app/blog.py
#!flask/bin/python
from flask import Flask, render_template
from flask_flatpages import FlatPages

DEBUG = True
FLATPAGES_AUTO_RELOAD = DEBUG
FLATPAGES_EXTENSION = '.md'

app = Flask(__name__)
app.config.from_object(__name__)
flatpages = FlatPages(app)

@app.route('/')
def index():
    pages = (p for p in flatpages if 'date' in p.meta)
    return render_template('index.html', pages=pages)

@app.route('/pages/<path:path>/')
def page(path):
    page = flatpages.get_or_404(path)
    return render_template('page.html', page=page)

if __name__ == '__main__':
    app.run(port=8000)

模版

在 Python 中直接生成 HTML 很繁琐并不好玩(那是上个世纪90年代的 PHP 搞的事情),在现代社会,我们使用模版引擎,Flask 已经自动配置好了 Jinja2 模版,使用方法 render_template() 来渲染模版就可以了。Flask 会默认在 templates 目录里中寻找模版,我们只需要创建几个模版文件就可以了,这里我们创建 base.html, index.html 和 page.html.

$ vi app/templates/base.html<!doctype html><html><head><meta charset="utf-8"><title>vpsee.com static blog</title></head><body><h1><a href="{{ url_for("index") }}">vpsee.com blog</a></h1>
    {% block content %}
    {% endblock content %}</body></html>

代码里 extends “base.html” 的意思是从 base.html 里继承基本的 “骨架”。

$ vi app/templates/index.html
{% extends "base.html" %}

{% block content %}
    <h2>List of pages<ul>
        {% for page in pages %}<li><a href="{{ url_for("page", path=page.path) }}">{{ page.title }}</a></li>
        {% else %}<li>No post.</li>
        {% endfor %}</ul>
{% endblock content %}
$ vi app/templates/page.html
{% extends "base.html" %}

{% block content %}
    <h2>{{ page.title }}</h2>
    {{ page.html|safe }}
{% endblock content %}

Flask-FlatPages 模块会默认从 pages 目录里寻找 .md 结尾的 Markdown 文档,所以我们把静态博客的内容都放在这个目录里:

$ vi app/pages/hello-world.md
title: Hello World
date: 2014-10-14
tags: [general, blog]

**Hello World**!

$ vi app/pages/test-flatpages.md
title: Test Flask FlatPages
date: 2014-10-15
tags: [python, flask]

Test [Flask-FlatPages](https://pythonhosted.org/Flask-FlatPages/)

运行

基本搞定,运行看看效果吧:

$ flask/bin/python app/blog.py
 * Running on http://127.0.0.1:8000/
 * Restarting with reloader

build a static blog with flask

静态化

到目前为止,上面的博客运行良好,但是有个问题,这个博客还不是 “静态” 的,没有生成任何 html 文件,不能直接放到 nginx/apache 这样的 web 服务器下用。所以我们需要另一个 Flask 模块 Frozen-Flask的帮助。

安装 Frozen-Flask:

$ flask/bin/pip install frozen-flask

修改 blog.py,导入 Flask-Frozen 模块,初始化 Freezer,使用 freezer.freeze() 生成静态 HTML:

$ vi app/blog.py
...
from flask_flatpages import FlatPages
from flask_frozen import Freezer
import sys
...
flatpages = FlatPages(app)
freezer = Freezer(app)
...
if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == "build":
        freezer.freeze()
    else:
        app.run(port=8000)

运行 blog.py build 后就在 app 目录下生成 build 目录,build 目录里面就是我们要的 HTML 静态文件:

$ flask/bin/python app/blog.py build

$ ls app/
blog.py   build     pages     static    templates

更清晰的目录结构如下:

$ tree app
app
├── blog.py
├── build
│   ├── index.html
│   └── pages
│       ├── hello-world
│       │   └── index.html
│       └── test-flatpages
│           └── index.html
├── pages
│   ├── hello-world.md
│   └── test-flatpages.md
├── static
└── templates
    ├── base.html
    ├── index.html
    └── page.html

创建GitHub技术博客全攻略

$
0
0

说明: 首先,你需要注册一个 github 账号,最好取一个有意义的名字,比如姓名全拼,昵称全拼,如果被占用,可以加上有意义的数字.
本文中假设用户名为  tiemaocsdn

1. 注册账号:

地址:  https://github.com/
输入账号、邮箱、密码,然后点击注册按钮.

图1 第1步

2. 初始设置
注册完成后,选择 Free免费账号完成设置。

图2 第2步

2.1 验证邮箱
请打开你的邮箱,查看发送给你的确认邮件,你需要验证邮箱后,后面生成的个人主页才会被接受和发布.

3. 创建页面仓库
地址:  https://github.com/new
这个仓库的名字需要和你的账号对应, 如  tiemaocsdn.github.io
输入基本信息,然后点击创建仓库.

图3 第3步

4. 进入项目设置页面
因为这个项目就是专门的放页面的,所以 master分支即可. 如果是你的某个仓库的页面,你需要设置到  gh-pages 分支中,关于这些,请参考本文末尾提到的参考资料.

图4 第4步


5. 进入自动页面生成器
在设置页面,下拉到底部, 默认分支(master)不用管.

图5 第5步


6.1 创建用户页面(即技术博客站点)
输入一些内容,可以自己编辑,也可以从readme加载:

图6-1 第6-1步


6.2 继续,选择主题

图6-2 第6-2步


7. 选择主题,然后发布
如下图所示,其实这些以后你都可以自己修改替换,这只是生成一些css,html,img到你的仓库里面.

图7 第7步


8. 查看效果
现在,你可以访问自己的GitHub.io 上的主页了,例如:
http://tiemaocsdn.github.io/
页面效果如下图所示:

图8 第8步


注意事项:

  • 如果报404,或者其他错误,请稍等,或者检查你的邮箱,看看构建失败的提示信息.
  • 比如我的,因为最初没有验证邮箱,结果收到了好几次发布失败的通知(如下面的邮件提示)。
  • 以后每次你提交(或在线修改后提交)文件到这个仓库,GitHub 都会自动为你构建,并发布。
  • 所以,有问题,请修改并提交某个文件,重新试试吧!
[plain]  view plaincopy
  1. The page build failed with the following error:  
  2.   
  3.   
  4. You need a verified email address in your GitHub account to publish Pages.  
  5. You can verify your email addresses from your Settings panel:  
  6.   
  7.   
  8.     https://github.com/settings/emails  
  9.   
  10.   
  11. If you have any questions please contact us at https://github.com/contact.  


下面是参考的一部分:

您可以通过GitHub的页面自动生成器快速为 项目,用户(User,比如你的账号),或组织(Organization, 比如 alibaba) 创建一个网站(其实就是技术博客啦)。

生成用户/组织的网页

要生成用户/组织的网站页面,你需要创建一个仓库(repository,代码库),名为: username.github.io 或 orgname.github.io ; 用户名或组织名 必须 是你自己的账号, 否则GitHub Pages 站点不会帮你生成(build,构建,编译,)。 GitHub的页面自动生成器可以通过仓库的设置页面(Settings page)看到。 您可以阅读更多 关于用户和组织页面的内容 。

警告: GitHub页面网站在互联网上是公开,即使其仓库是私有的。 如果你有敏感数据在页面仓库中,您可能需要在发布之前删除他们。

参考地址:

如果你看到这里,如果你已登录, 如果觉得对你有帮助,请点击下面的  “顶” 按钮.

如果有问题,有建议,请留言。

赶快试试吧!

GitHub.io技术博客的好处: 写博客,你可以自己定义JS,CSS,图片,嵌入iframe 显示代码示例,什么你都可以自定义,什么标签都允许,而在其他的技术博客站点,因为安全限制,很多是不允许的。



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



在Debian下搭建基于Apache-Php-MySQL的wordpress博客

$
0
0

wordpress是一个流行的博客搭建框架,为不会html,css和js的人提供了搭建博客的便捷方式.我这里是在我的笔记本上搭建了一个wordpress博客,这里把详细的搭建过程写出来.

我的系统信息如下:

Selection_001

具体的操作过程如下描述.

1.安装apache2服务器

Selection_003

其中apache2-doc是apache服务器的说明和配置文件,libapache2-mod-php5是apache的php模块库文件.


安装成功后,重启apache2服务器,

Selection_001

此时在浏览器地址栏里面输入http://localhost,则会看到如下的页面,提示我们apache2服务器已经安装成功.

Selection_002

 

2.关于apache2的配置信息:

a.apache2的配置文件目录是/etc/apache2.在debian下,配置文件被打散分到了该目录下的几个子文件夹中.可以看该目录下的文件:

Selection_002

其中apache2.conf 是主配置文件,该目录下还有ports.conf文件用来配置服务器的监听端口.此外mod-enabled和sites-enabled和conf-enabled子目录下分别有一个.conf文件,详细的配置说明可以看相应的说明.

b.apache2安装时会创建一个叫做www-data的用户,所有apache相关的进程都由该用户来启动执行.可以在浏览器里面访问localhost的时候,用top命令查看:

Selection_003

上图中第5条记录即为apache2服务器的进程开销情况.

c.apache2的默认网页和脚本存放目录为/var/www/html,在该目录下存放的网页(除了index页面)都可以通过http://localhost/filename访问到,如该目录下有个aboutme.html,则可以在浏览器里输入http://localhost/aboutme.html访问.

3.安装php:

Selection_007

其中php5-mysql是php和mysql数据库的接口,为了使用mysql数据库必须安装这个包.

安装完成后,可以通过如下方法检查php的安装是否成功:

a.在/var/www/html目录下,编写如下内容的文件phpinfo.php:

<?php phpinfo(); ?>

然后在浏览器中访问该页面:http://localhost/phpinfo.php,如果出现如下页面,则说明php安装已经成功.

Selection_008往下拉一下网页右侧滚动条,就可以看到下面是php支持的各个模块和组件.看起来相当多.

4.安装mysql:

Selection_009

安装完成后,刷新刚才的phpinfo页面,往下拉到中间位置的时候,可以看到mysql和mysqli,说明msyql也已经安装成功了. Selection_010

 

5.下载wordpress压缩文件:

访问http://cn.wordpress.org,如下图所示.在右侧中间位置有压缩包供下载,点击下载即可.

Selection_011

也可复制该链接地址,用wget下载(感觉好像用wget下载比较快):


Selection_012下载后解压该文件:

Selection_013

解压后的文件放在wordpress文件夹下,可以看看里面的内容:

Selection_004

可以看到大多都是以wp开头的文件或文件夹,这些文件夹保存了配置博客的脚本和展示给访问者的页面框架,而其他的信息则保存在数据库中.

因为我们默认的网页存放目录是/var/www/html,所以要将该文件夹内文件移动到该目录下才生效,所以执行如下移动操作:

mv -R wordpress /var/www/html

该操作会用wordpress目录替换原来的html目录.

现在在浏览器中打开http://localhost,就会看到开始wordpress的配置的页面了:

Selection_016


然后按照步提示,在mysql创建相应的wordpress数据库,整个博客就算搭建完成了!

下面是我搭建的博客(随便从网上抄了点内容…):

Selection_017

(-完-)




作者:yiranfantexi44 发表于2014-12-5 19:23:31 原文链接
阅读:104 评论:0 查看评论

让博客园博客自动生成章节目录索引 - 薰衣草的旋律

$
0
0

一个好的博文除了博文的质量要好以外,好的组织结构也能让读者阅读的更加舒服与方便,我看园子里面有一些园友的博文都是分章节的,并且在博文的前面都带有章节的目录索引,点击索引之后会跳转到相应的章节阅读,并且还可以回到目录顶端,其中 Fish Li的博文就是这种组织,当然这种结构如果是在写博文的时候人工设置那是非常麻烦的,无疑是增加了写作人的工作量。如果能自动生成章节索引岂不是节省了一大堆工作量。本来想通过FireBug看看Fish Li源码是怎么实现的,但是好像js是加密过的。那我就自己动手了,其实也没多少代码,很简单。

<script language="javascript" type="text/javascript">
//生成目录索引列表
function GenerateContentList()
{
var jquery_h3_list = $('#cnblogs_post_body h3');//如果你的章节标题不是h3,只需要将这里的h3换掉即可
if(jquery_h3_list.length>0)
{
var content = '<a name="_labelTop"></a>';
content += '<div id="navCategory">';
content += '<p style="font-size:18px"><b>阅读目录</b></p>';
content += '<ul>';
for(var i =0;i<jquery_h3_list.length;i++)
{
var go_to_top = '<div style="text-align: right"><a href="#_labelTop">回到顶部</a><a name="_label' + i + '"></a></div>';
$(jquery_h3_list[i]).before(go_to_top);
var li_content = '<li><a href="#_label' + i + '">' + $(jquery_h3_list[i]).text() + '</a></li>';
content += li_content;
}
content += '</ul>';
content += '</div>';
if($('#cnblogs_post_body').length != 0 )
{
$($('#cnblogs_post_body')[0]).prepend(content);
}
}
}
GenerateContentList();
</script>

使用方法:登录到博客园之后,打开博客园的后台管理,切换到“设置”选项卡,将上面的代码,粘贴到 “页脚HTML代码” 区保存即可。

注意:上述js代码中提取的h3作为章节的标题,如果你的标题不是h3请在代码注释的地方自行修改。该代码除了在文章的最开始生成目录索引之外,还会在每一个章节最后右下角(也就是下一个章节标题的右上角)会生成一个“回到顶部”的链接,以方便读者回到目录。本篇文章的目录结构就是自动生成的效果,如果你觉得有用,就赶快试用一下吧。

章节1

这里是章节1的内容

章节2

这里是章节2的内容

章节3

这里是章节3的内容

章节4

小小代码,不值一提,如果您觉得对您还有一点用,就点个赞支持一下吧。


本文链接: 让博客园博客自动生成章节目录索引,转载请注明。


本博客 Nginx 配置之性能篇

$
0
0

在介绍完我博客(imququ.com)的 Nginx 配置中 与安全有关的一些配置后,这篇文章继续介绍与性能有关的一些配置。WEB 性能优化是一个系统工程,涵盖很多方面,做好其中某个环节并不意味性能就能变好,但可以肯定地说,如果某个环节做得很糟糕,那么结果一定会变差。

首先说明下,本文提到的一些 Nginx 配置,需要较高版本 Linux 内核才支持。往往在实际生产环境中,升级服务器内核并不是一件容易的事。为了获得最好的性能,有些升级还是必须的。但往往服务器运维和项目开发并不在一个团队,一方追求稳定不出事故,另一方希望提升性能,本来就是矛盾的。好在我们折腾自己 VPS 时,可以无视这些限制。

TCP 优化

Nginx 关于 TCP 的优化基本都是修改系统内核提供的配置项,所以跟具体的 Linux 版本和系统配置有关,我对这一块还不是非常熟悉,这里只能简单介绍下:

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    keepalive_timeout 60;
    ... ...
}

第一行的 sendfile配置可以提高 Nginx 静态资源托管效率。sendfile 是一个系统调用,直接在内核空间完成文件发送,不需要先 read 再 write,没有上下文切换开销。

TCP_NOPUSH 是 FreeBSD 的一个 socket 选项,对应 Linux 的 TCP_CORK,Nginx 里统一用 tcp_nopush来控制它,并且只有在启用了 sendfile 之后才生效。启用它之后,数据包会累计到一定大小之后才会发送,减小了额外开销,提高网络效率。

TCP_NODELAY 也是一个 socket 选项,启用后会禁用 Nagle 算法,尽快发送数据,可以节约 200ms。Nginx 只会针对处于 keep-alive 状态的 TCP 连接才会启用 tcp_nodelay

可以看到 TCP_NOPUSH 是要等数据包累积到一定大小才发送,TCP_NODELAY 是要尽快发送,二者相互矛盾。实际上,它们确实可以一起用,最终的效果是先填满包,再尽快发送。

关于这部分内容的更多介绍可以看这篇文章: NGINX OPTIMIZATION: UNDERSTANDING SENDFILE, TCP_NODELAY AND TCP_NOPUSH

配置最后一行用来指定服务端为每个 TCP 连接最多可以保持多长时间。Nginx 的默认值是 75 秒,有些浏览器最多只保持 60 秒,所以我统一设置为 60。

另外,还有一个 TCP 优化策略叫 TCP Fast Open(TFO),这里先介绍下,配置在后面贴出。TFO 的作用是用来优化 TCP 握手过程。客户端第一次建立连接还是要走三次握手,所不同的是客户端在第一个 SYN 会设置一个 Fast Open 标识,服务端会生成 Fast Open Cookie 并放在 SYN-ACK 里,然后客户端就可以把这个 Cookie 存起来供之后的 SYN 用。下面这个图形象地描述了这个过程:

tcp fast open

关于 TCP Fast Open 的更多信息,可以查看 RFC7413,或者这篇文章: Shaving your RTT with TCP Fast Open

开启 Gzip

我们在上线前,代码(JS、CSS 和 HTML)会做压缩,图片也会做压缩(PNGOUT、Pngcrush、JpegOptim、Gifsicle 等)。对于文本文件,在服务端发送响应之前进行 GZip 压缩也很重要,通常压缩后的文本大小会减小到原来的 1/4 - 1/3。下面是我的配置:

http {
    gzip             on;
    gzip_vary        on;

    gzip_comp_level  6;
    gzip_buffers     16 8k;

    gzip_min_length  1000;
    gzip_proxied     any;
    gzip_disable     "msie6";

    gzip_types       text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;
    ... ...
}

这部分内容比较简单,只有两个地方需要解释下:

gzip_vary用来输出 Vary 响应头,用来解决某些缓存服务的一个问题,详情请看我之前的博客: HTTP 协议中 Vary 的一些研究

gzip_disable指令接受一个正则表达式,当请求头中的 UserAgent 字段满足这个正则时,响应不会启用 GZip,这是为了解决在某些浏览器启用 GZip 带来的问题。特别地,指令值 msie6等价于 MSIE [4-6]\.,但性能更好一些。另外,Nginx 0.8.11 后, msie6并不会匹配 UA 包含 SV1的 IE6(例如 Windows XP SP2 上的 IE6),因为这个版本的 IE6 已经修复了关于 GZip 的若干 Bug。

开启缓存

优化代码逻辑的极限是移除所有逻辑;优化请求的极限是不发送任何请求。这两点通过缓存都可以实现。

服务端

我的博客更新并不频繁,评论部分也早就换成了 Disqus,所以完全可以将页面静态化,这样就省掉了所有代码逻辑和数据库开销。实现静态化有很多种方案,我直接用的是 Nginx 的 proxy_cache:

proxy_cache_path    /home/jerry/cache/nginx/proxy_cache_path levels=1:2 keys_zone=pnc:300m inactive=7d max_size=10g;
proxy_temp_path     /home/jerry/cache/nginx/proxy_temp_path;
proxy_cache_key     $host$uri$is_args$args;

server {
    location / {
        resolver                 127.0.0.1;  
        proxy_cache              pnc;
        proxy_cache_valid        200 304 2h;
        proxy_cache_lock         on;
        proxy_cache_lock_timeout 5s;
        proxy_cache_use_stale    updating error timeout invalid_header http_500 http_502;

        proxy_http_version       1.1;

        proxy_ignore_headers     Set-Cookie;
        ... ...
    }
    ... ...
}

首先,在配置最外层定义一个缓存目录,并指定名称(keys_zone)和其他属性,这样在配置 proxy_pass 时,就可以使用这个缓存了。这里我对状态值等于 200 和 304 的响应缓存了 2 小时。

默认情况下,如果响应头里有 Set-Cookie 字段,Nginx 并不会缓存这次响应,因为它认为这次响应的内容是因人而异的。我的博客中,这个 Set-Cookie 对于用户来说没有用,也不会影响输出内容,所以我通过配置 proxy_ignore_header移除了它。

客户端

服务端在输出响应时,可以通过响应头输出一些与缓存有关的信息,从而达到少发或不发请求的目的。HTTP/1.1 的缓存机制稍微有点复杂,这里简单介绍下:

首先:服务端可以通过响应头里的 Last-Modified(最后修改时间) 或者 ETag(内容特征) 标记实体。浏览器会存下这些标记,并在下次请求时带上 If-Modified-Since: 上次 Last-Modified 的内容If-None-Match: 上次 ETag 的内容,询问服务端资源是否过期。如果服务端发现并没有过期,直接返回一个状态码为 304、正文为空的响应,告知浏览器使用本地缓存;如果资源有更新,服务端返回状态码 200、新的 Last-Modified、Etag 和正文。这个过程被称之为 HTTP 的协商缓存,通常也叫做弱缓存。

可以看到协商缓存并不会节省连接数,但是在缓存生效时,会大幅减小传输内容(304 响应没有正文,一般只有几百字节)。另外为什么有两个响应头都可以用来实现协商缓存呢?这是因为一开始用的 Last-Modified有两个问题:1)只能精确到秒,1 秒内的多次变化反映不出来;2)时间采用绝对值,如果服务端 / 客户端时间不对都可能导致缓存失效。HTTP/1.1 并没有规定 ETag的生成规则,而一般实现者都是对资源内容做摘要,能解决前面两个问题。

另外一种缓存机制是服务端通过响应头告诉浏览器,在什么时间之前(Expires)或在多长时间之内(Cache-Control: Max-age=xxx),不要再请求服务器了。这个机制我们通常称之为 HTTP 的强缓存。

一旦资源命中强缓存规则后,再次访问完全没有 HTTP 请求(Chrome 开发者工具的 Network 面板依然会显示请求,但是会注明 from cache;Firefox 的 firebug 也类似,会注明 BFCache),这会大幅提升性能。所以我们一般会对 CSS、JS、图片等资源使用强缓存,而入口文件(HTML)一般使用协商缓存或不缓存,这样可以通过修改入口文件中对强缓存资源的引入 URL 来达到即时更新的目的。

这里也解释下为什么有了 Expire,还要有 Cache-Control。也有两个原因:1)Cache-Control 功能更强大,对缓存的控制能力更强;2)Cache-Control 采用的 max-age 是相对时间,不受服务端 / 客户端时间不对的影响。

另外关于浏览器的刷新(F5 / cmd + r)和强刷(Ctrl + F5 / shift + cmd +r):普通刷新会使用协商缓存,忽略强缓存;强刷会忽略浏览器所有缓存(并且请求头会携带 Cache-Control:no-cache 和 Pragma:no-cache,用来通知所有中间节点忽略缓存)。只有从地址栏或收藏夹输入网址、点击链接等情况下,浏览器才会使用强缓存。

默认情况下,Nginx 对于静态资源都会输出 Last-Modified,而 ETagExpireCache-Control则需要自己配置:

location ~ ^/static/ {
    root      /home/jerry/www/blog/www;
    etag      on;
    expires   max;
}

expires指令可以指定具体的 max-age,例如 10y代表 10 年,如果指定为 max,最终输出的 Expires会是 2037 年最后一天, Cache-Controlmax-age会是 10 年(准确说是 3650 天,315360000 秒)。

使用 SPDY(HTTP/2)

我的博客之前多次讲到过 HTTP/2(SPDY),现阶段 Nginx 只支持 SPDY/3.1,这样配置就可以启用了(编译 Nginx 时需要加上 --with-http_spdy_module 和 --with-http_ssl_module):

server {
    listen             443 ssl spdy fastopen=3;
    spdy_headers_comp  6;
    ... ...
}

那个 fastopen=3用来开启前面介绍过的 TCP Fast Open 功能。3 代表最多只能有 3 个未经三次握手的 TCP 链接在排队。超过这个限制,服务端会退化到采用普通的 TCP 握手流程。这是为了减少资源耗尽攻击:TFO 可以在第一次 SYN 的时候发送 HTTP 请求,而服务端会校验 Fast Open Cookie(FOC),如果通过就开始处理请求。如果不加限制,恶意客户端可以利用合法的 FOC 发送大量请求耗光服务端资源。

HTTPS 优化

建立 HTTPS 连接本身就慢(多了获取证书、校验证书、TLS 握手等等步骤),如果没有优化好只能是慢上加慢。

server {
    ssl_session_cache          shared:SSL:10m;
    ssl_session_timeout        10m;

    ssl_session_tickets        on;

    ssl_stapling               on;
    ssl_stapling_verify        on;
    resolver 8.8.4.4 8.8.8.8 valid=300s;
    resolver_timeout 10s;
    ... ...
}

我的这部分配置就两部分内容:TLS 会话恢复和 OCSP stapling。

TLS 会话恢复的目的是为了简化 TLS 握手,有两种方案:Session Cache 和 Session Ticket。他们都是将之前握手的 Session 存起来供后续连接使用,所不同是 Cache 存在服务端,占用服务端资源;Ticket 存在客户端,不占用服务端资源。另外目前主流浏览器都支持 Session Cache,而 Session Ticket 的支持度一般。

ssl_stapling开始的四行用来配置 OCSP stapling 策略。浏览器可能会在建立 TLS 连接时在线验证证书有效性,从而阻塞 TLS 握手,拖慢整体速度。OCSP stapling 是一种优化措施,服务端通过它可以在证书链中封装证书颁发机构的 OCSP(Online Certificate Status Protocol)响应,从而让浏览器跳过在线查询。服务端获取 OCSP 一方面更快(因为服务端一般有更好的网络环境),另一方面可以更好地缓存。有关 OCSP stapling 的详细介绍,可以 看这里

好了,我的博客关于安全和性能两部分 Nginx 配置终于都写完了。实际上很多策略没办法严格区分是为了安全还是性能,比如 HSTS 和 CHACHA20_POLY1305,两方面都有考虑,所以写的时候比较纠结,早知道就合成一篇来写了。

本文链接: https://www.imququ.com/post/my-nginx-conf-for-wpo.html参与讨论

推荐: 领略前端技术 阅读奇舞周刊

WordPress 全球份额已达25%:早已不是博客工具

$
0
0

新浪科技讯北京时间11月9日早间消息,本周日,内容管理平台WordPress迎来了重要一天。来自W3Techs的数据显示,目前已有1/4的互联网网站基于WordPress平台。

W3Techs表示:“在我们知晓内容管理系统的网站中,有58.7%的网站使用WordPress。这占所有网站的25.0%。”尽管这一数字每月都会波动,但整体来看,WordPress的市场份额正在稳步增长。

WordPress的开发者Automattic联合创始人马特·穆伦维格(Matt Mullenweg)表示:“到今年年底,我们很高兴市场份额突破25%,更大的机会在于,有57%网站尚未使用任何明确的内容管理系统。因此我认为,我 们未来仍有很大的增长空间。

实际上,过去几年中,WordPress的市场份额并没有太大增长。过去一年该公司的份额甚至出现下跌。

然而,控制超过一半的市场份额仍是杰出的成就,而这样的领先地位往往会招致各种攻击。在产品生命周期中,WordPress也遭遇了一系列的信息安全问题。(维金)

[Python爬虫] Selenium爬取新浪微博客户端用户信息、热点话题及评论 (上)

$
0
0

一. 文章介绍


前一篇文章" [python爬虫] Selenium爬取新浪微博内容及用户信息"简单讲述了如何爬取新浪微博手机端用户信息和微博信息。
用户信息:包括用户ID、用户名、微博数、粉丝数、关注数等。
微博信息:包括转发或原创、点赞数、转发数、评论数、发布时间、微博内容等。


它主要通过从文本txt中读取用户id,通过"URL+用户ID" 访问个人网站,如柳岩:
         http://weibo.cn/guangxianliuya
因为手机端数据相对精简简单,所以采用输入用户的形式依次爬取各个明星的信息。
而这篇文章主要爬取客户端的微博信息,相对信息更多;同时登录微博后在输入框中搜索热点话题,然后依次爬取微博信息和对应的评论。这篇文章的输出如下图所示:


PS:注意这篇文章爬取微博内容和评论的时候,由于它是动态加载的,故爬取失败,但思考可以参考。后面下篇会进行解决,如果实在不行只能爬取手机端的信息了。


二. 核心代码


这篇文章打算先给出完整代码,再进行讲解的方法:
1.LoginWeibo(username, password) 登录微博,自动输入用户名和密码
2.VisitPersonPage(user_id) 访问跟人网站,获取个人信息,通过如下网址访问柳岩:
      http://weibo.cn/guangxianliuyan
3.GetComment(key) 获取微博信息及评论信息,获取输入框按钮进行搜索
获取微博内容评论是注意翻页功能

# coding=utf-8"""  
Created on 2016-04-24 @author: Eastmount
功能: 爬取新浪微博用户的信息及微博评论
网址:http://weibo.cn/ 数据量更小 相对http://weibo.com/
"""    

import time            
import re            
import os    
import sys  
import codecs  
import shutil
import urllib 
from selenium import webdriver        
from selenium.webdriver.common.keys import Keys        
import selenium.webdriver.support.ui as ui        
from selenium.webdriver.common.action_chains import ActionChains

#先调用无界面浏览器PhantomJS或Firefox    
#driver = webdriver.PhantomJS(executable_path="G:\phantomjs-1.9.1-windows\phantomjs.exe")    
driver = webdriver.Firefox()
wait = ui.WebDriverWait(driver,10)

#全局变量 文件操作读写信息
inforead = codecs.open("SinaWeibo_List_best_1.txt", 'r', 'utf-8')
infofile = codecs.open("SinaWeibo_Info_best_1.txt", 'a', 'utf-8')

#********************************************************************************
#                            第一步: 登陆weibo.cn 
#        该方法针对weibo.cn有效(明文形式传输数据) weibo.com见学弟设置POST和Header方法
#                LoginWeibo(username, password) 参数用户名 密码
#********************************************************************************

def LoginWeibo(username, password):
    try:
        #输入用户名/密码登录
        print u'准备登陆Weibo.cn网站...'
        driver.get("http://login.sina.com.cn/")
        elem_user = driver.find_element_by_name("username")
        elem_user.send_keys(username) #用户名
        elem_pwd = driver.find_element_by_name("password")
        elem_pwd.send_keys(password)  #密码
        #elem_rem = driver.find_element_by_name("safe_login")
        #elem_rem.click()             #安全登录

        #重点: 暂停时间输入验证码(http://login.weibo.cn/login/ 手机端需要)
        time.sleep(20)
        
        elem_sub = driver.find_element_by_xpath("//input[@class='smb_btn']")
        elem_sub.click()              #点击登陆 因无name属性
        time.sleep(2)
        
        #获取Coockie 推荐资料:http://www.cnblogs.com/fnng/p/3269450.html
        print driver.current_url
        print driver.get_cookies()  #获得cookie信息 dict存储
        print u'输出Cookie键值对信息:'
        for cookie in driver.get_cookies(): 
            #print cookie
            for key in cookie:
                print key, cookie[key]
        #driver.get_cookies()类型list 仅包含一个元素cookie类型dict
        print u'登陆成功...'
    except Exception,e:      
        print "Error: ",e
    finally:    
        print u'End LoginWeibo!\n\n'


#********************************************************************************
#                  第二步: 访问个人页面http://weibo.cn/5824697471并获取信息
#                                VisitPersonPage()
#        编码常见错误 UnicodeEncodeError: 'ascii' codec can't encode characters 
#********************************************************************************

def VisitPersonPage(user_id):

    try:
        global infofile       #全局文件变量
        url = "http://weibo.com/" + user_id
        driver.get(url)
        print u'准备访问个人网站.....', url
        print u'个人详细信息'
        #用户id
        print u'用户id: ' + user_id

        #昵称
        str_name = driver.find_element_by_xpath("//div[@class='pf_username']/h1")
        name = str_name.text        #str_name.text是unicode编码类型
        print u'昵称: ', name
        #关注数 粉丝数 微博数 <td class='S_line1'>
        str_elem = driver.find_elements_by_xpath("//table[@class='tb_counter']/tbody/tr/td/a")
        str_gz = str_elem[0].text    #关注数
        num_gz = re.findall(r'(\w*[0-9]+)\w*', str_gz)
        str_fs = str_elem[1].text    #粉丝数
        num_fs = re.findall(r'(\w*[0-9]+)\w*', str_fs)
        str_wb = str_elem[2].text    #微博数
        num_wb = re.findall(r'(\w*[0-9]+)\w*', str_wb)
        print u'关注数: ', num_gz[0]
        print u'粉丝数: ', num_fs[0]
        print u'微博数: ', num_wb[0]

        #文件操作写入信息
        infofile.write('=====================================================================\r\n')
        infofile.write(u'用户: ' + user_id + '\r\n')
        infofile.write(u'昵称: ' + name + '\r\n')
        infofile.write(u'关注数: ' + str(num_gz[0]) + '\r\n')
        infofile.write(u'粉丝数: ' + str(num_fs[0]) + '\r\n')
        infofile.write(u'微博数: ' + str(num_wb[0]) + '\r\n')
    except Exception,e:      
        print "Error: ",e
    finally:    
        print u'VisitPersonPage!\n\n'
        print '**********************************************\n'
        infofile.write('=====================================================================\r\n\r\n')


#********************************************************************************
#                  第三步: 访问http://s.weibo.com/页面搜索热点信息
#                  爬取微博信息及评论,注意评论翻页的效果和微博的数量
#********************************************************************************    

def GetComment(key):
    try:
        global infofile       #全局文件变量
        driver.get("http://s.weibo.com/")
        print u'搜索热点主题:', key

        #输入主题并点击搜索
        item_inp = driver.find_element_by_xpath("//input[@class='searchInp_form']")
        item_inp.send_keys(key)
        item_inp.send_keys(Keys.RETURN)    #采用点击回车直接搜索

        #内容
        #content = driver.find_elements_by_xpath("//div[@class='content clearfix']/div/p")
        content = driver.find_elements_by_xpath("//p[@class='comment_txt']")
        print content
        i = 0
        print u'长度', len(content)
        while i<len(content):
            print '微博信息:'
            print content[i].text
            infofile.write(u'微博信息:\r\n')
            infofile.write(content[i].text + '\r\n')
            i = i + 1

        #评论 由于评论是动态加载,爬取失败
        #Error:  list index out of range
        comment = driver.find_elements_by_xpath("//p[@class='list_ul']/dl/dd/div[0]")
        j = 0
        while j<10:
            print comment[j].text
            j = j + 1


    except Exception,e:      
        print "Error: ",e
    finally:    
        print u'VisitPersonPage!\n\n'
        print '**********************************************\n'

             
#*******************************************************************************
#                                程序入口 预先调用
#         注意: 因为sina微博增加了验证码,但是你用Firefox登陆输入验证码
#         直接跳转到明星微博那部分,即: http://weibo.cn/guangxianliuyan
#*******************************************************************************
    
if __name__ == '__main__':

    #定义变量
    username = '1520161****'             #输入你的用户名
    password = '*********'               #输入你的密码

    #操作函数
    LoginWeibo(username, password)       #登陆微博

    #在if __name__ == '__main__':引用全局变量不需要定义 global inforead 省略即可
    print 'Read file:'
    user_id = inforead.readline()
    while user_id!="":
        user_id = user_id.rstrip('\r\n')
        print user_id
        VisitPersonPage(user_id)         #访问个人页面http://weibo.cn/guangxianliuyan
        user_id = inforead.readline()
        #break

    #搜索热点微博 爬取评论
    key = u'欢乐颂' 
    GetComment(key)
    infofile.close()
    inforead.close()

PS:后面是具体的实现过程分析讲解,如果你只需要代码,上面就是所有完整代码,但建议也看看后面的分析过程,虽然是傻瓜式爬虫,但至少能用,而且方法类似。



三. 登录入口


新浪微博登录常用接口: http://login.sina.com.cn/ 
对应主界面: http://weibo.com/
但是个人建议采用手机端微博入口: http://login.weibo.cn/login/  
对应主界面: http://weibo.cn/
通过比较下面两张图,分别是PC端和手机端,可以发现内容基本一致:



手机端下图所示,其中图片相对更小,同时内容更精简。






四. 分析-登录微博LoginWeibo


登录过程如下图所示,先通过函数获取用户名、密码、登录按钮结点,然后再自动输入信息并登录。如果需要输入验证码,也可以在手动输入。


对应源码:
#********************************************************************************
#                            第一步: 登陆weibo.cn 
#        该方法针对weibo.cn有效(明文形式传输数据) weibo.com见学弟设置POST和Header方法
#                LoginWeibo(username, password) 参数用户名 密码
#********************************************************************************

def LoginWeibo(username, password):
    try:
        #输入用户名/密码登录
        print u'准备登陆Weibo.cn网站...'
        driver.get("http://login.sina.com.cn/")
        elem_user = driver.find_element_by_name("username")
        elem_user.send_keys(username) #用户名
        elem_pwd = driver.find_element_by_name("password")
        elem_pwd.send_keys(password)  #密码
        #elem_rem = driver.find_element_by_name("safe_login")
        #elem_rem.click()             #安全登录

        #重点: 暂停时间输入验证码(http://login.weibo.cn/login/ 手机端需要)
        time.sleep(20)
        
        elem_sub = driver.find_element_by_xpath("//input[@class='smb_btn']")
        elem_sub.click()              #点击登陆 因无name属性
        time.sleep(2)
        
        #获取Coockie 推荐资料:http://www.cnblogs.com/fnng/p/3269450.html
        print driver.current_url
        print driver.get_cookies()  #获得cookie信息 dict存储
        print u'输出Cookie键值对信息:'
        for cookie in driver.get_cookies(): 
            #print cookie
            for key in cookie:
                print key, cookie[key]
        #driver.get_cookies()类型list 仅包含一个元素cookie类型dict
        print u'登陆成功...'
    except Exception,e:      
        print "Error: ",e
    finally:    
        print u'End LoginWeibo!\n\n'

分析网页结点如下图所示:


核心代码:
        elem_user = driver.find_element_by_name("username")
        elem_user.send_keys(username)     #用户名
        elem_pwd = driver.find_element_by_name("password")
        elem_pwd.send_keys(password)      #密码
        elem_sub = driver.find_element_by_xpath("//input[@class='smb_btn']")
        elem_sub.click()                               #点击登陆

登录后跳转到下面页面:





五. 分析-爬取用户个人信息VisitPersonPage


通过URL+用户ID的形式访问信息,访问页面如下图所示:


代码如下所示:
#********************************************************************************
#                  第二步: 访问个人页面http://weibo.cn/5824697471并获取信息
#                                VisitPersonPage()
#        编码常见错误 UnicodeEncodeError: 'ascii' codec can't encode characters 
#********************************************************************************

def VisitPersonPage(user_id):

    try:
        global infofile       #全局文件变量
        url = "http://weibo.com/" + user_id
        driver.get(url)
        print u'准备访问个人网站.....', url
        print u'个人详细信息'
        #用户id
        print u'用户id: ' + user_id

        #昵称
        str_name = driver.find_element_by_xpath("//div[@class='pf_username']/h1")
        name = str_name.text        #str_name.text是unicode编码类型
        print u'昵称: ', name
        #关注数 粉丝数 微博数 <td class='S_line1'>
        str_elem = driver.find_elements_by_xpath("//table[@class='tb_counter']/tbody/tr/td/a")
        str_gz = str_elem[0].text    #关注数
        num_gz = re.findall(r'(\w*[0-9]+)\w*', str_gz)
        str_fs = str_elem[1].text    #粉丝数
        num_fs = re.findall(r'(\w*[0-9]+)\w*', str_fs)
        str_wb = str_elem[2].text    #微博数
        num_wb = re.findall(r'(\w*[0-9]+)\w*', str_wb)
        print u'关注数: ', num_gz[0]
        print u'粉丝数: ', num_fs[0]
        print u'微博数: ', num_wb[0]

        #文件操作写入信息
        infofile.write('=====================================================================\r\n')
        infofile.write(u'用户: ' + user_id + '\r\n')
        infofile.write(u'昵称: ' + name + '\r\n')
        infofile.write(u'关注数: ' + str(num_gz[0]) + '\r\n')
        infofile.write(u'粉丝数: ' + str(num_fs[0]) + '\r\n')
        infofile.write(u'微博数: ' + str(num_wb[0]) + '\r\n')
    except Exception,e:      
        print "Error: ",e
    finally:    
        print u'VisitPersonPage!\n\n'
        print '**********************************************\n'
其中SinaWeibo_List_best_1.txt中仅包含两个用户id的情况:


该部分输出如下图所示:


分析页面DOM树结构如下图所示:



同时这里只获取简单的信息,详细信息还可以自动点击"查看更多"进行获取:



六. 分析-爬取微博和评论信息GetComment


该部分代码如下:
#********************************************************************************
#                  第三步: 访问http://s.weibo.com/页面搜索热点信息
#                  爬取微博信息及评论,注意评论翻页的效果和微博的数量
#********************************************************************************    

def GetComment(key):
    try:
        global infofile       #全局文件变量
        driver.get("http://s.weibo.com/")
        print u'搜索热点主题:', key

        #输入主题并点击搜索
        item_inp = driver.find_element_by_xpath("//input[@class='searchInp_form']")
        item_inp.send_keys(key)
        item_inp.send_keys(Keys.RETURN)    #采用点击回车直接搜索

        #内容
        #content = driver.find_elements_by_xpath("//div[@class='content clearfix']/div/p")
        content = driver.find_elements_by_xpath("//p[@class='comment_txt']")
        print content
        i = 0
        print u'长度', len(content)
        while i<len(content):
            print '微博信息:'
            print content[i].text
            infofile.write(u'微博信息:\r\n')
            infofile.write(content[i].text + '\r\n')
            i = i + 1

        #评论 由于评论是动态加载,爬取失败
        #Error:  list index out of range
        comment = driver.find_elements_by_xpath("//p[@class='list_ul']/dl/dd/div[0]")
        j = 0
        while j<10:
            print comment[j].text
            j = j + 1


    except Exception,e:      
        print "Error: ",e
    finally:    
        print u'VisitPersonPage!\n\n'
        print '**********************************************\n'
通过访问该URL进行热点搜索: http://s.weibo.com/



再通过核定代码输入主题如“欢乐颂”并点击回车键,分析节点方法与前面类似:
        item_inp = driver.find_element_by_xpath("//input[@class='searchInp_form']")
        item_inp.send_keys(key)
        item_inp.send_keys(Keys.RETURN)    #采用点击回车直接搜索

自动返回搜索结果如下图所示:


分析DOM树结构如下,右键浏览器"审查元素":


分析具体的信息如下所示:


但爬取博客过程中,总显示空值,不知道为什么,怀疑是动态加载的。
content = driver.find_elements_by_xpath("//div[@class='content clearfix']/div/p")
content = driver.find_elements_by_xpath("//p[@class='comment_txt']")

评论信息需要点击"评论1897"才能进行加载:


对应源码如下所示,它是动态进行加载的:





如图,审查元素点击"评论"可以发现它是通过JavaScript加载,这就比较头疼了。



PS:最后希望文章对你有所帮助!其实方法很简单,希望你能理解这种思想,如何分析HTML源码及DOM树结构,然后动态获取自己需要的信息。
关于如何动态爬取评论部分我还在研究当中,实在不行可能只能通过手机端进行爬取了。同时因为最近太忙,只能写写这种效率很低的傻瓜式爬虫,后面毕业了会深入研究爬虫知识。但至少代码能运行,可以爬取信息,当前阶段就非常不错了。不喜勿喷,加油~

(By:Eastmount 2016-04-24 早上7点半   http://blog.csdn.net/eastmount/ )


作者:Eastmount 发表于2016/4/24 7:29:34 原文链接
阅读:5 评论:0 查看评论

hexo零成本搭建个人博客

$
0
0

hexo作者 台湾人

hexo优势

不可思议的快速 ─ 只要一眨眼静态文件即生成完成
支持 Markdown仅需一道指令即可部署到 GitHub Pages

兼容于 Windows, Mac & Linux
不需要域名与服务器
轻量易用

首先需要安装以下程序:
Node.js安装非常简单,一路Next即可。

Git安装建议勾选Git Bash Here
,方便以后的操作。其他一路Next即可。

Installation安装

在选中的文件夹上鼠标右键git bush

$ npm install hexo-cli -g

Setup your blog

$ hexo init blog//创建blog文件夹
$ cd blog//打开blog文件夹

Start the server

$ hexo server//启动服务器

或者

$ hexo server -p 4000
#-p 4000 可以不写 默认是4000 但有时候4000端口占用就要用8888这样自定义的端口了)

访问localhost:4000预览,退出server用Ctrl+c

Create a new post创建新文章题目为hello hexo

$ hexo new "Hello Hexo"

 编辑文章
hexo new "My New Post"会在 ..source_posts目录下生成一个markdown文件:My-New-Post.md
可以使用一个支持markdown语法的编辑器(比如 Sublimeatom)来编辑该文章

title: my new post #可以改成中文的,如“新文章”
date: 2013-05-29 07:56:29 #发表日期,一般不改动
categories: blog #文章文类
tags: [博客,文章] #文章标签,多于一项时用这种格式
--
#这里是正文,用markdown写,

markdown 入门指南

$ hexo generate
$ hexo deploy

同步到github。访问网站看看效果。
Generate static files生成文章

$ hexo clean #经常要用的命令
$ hexo generate
$ hexo deploy  #Deploy after generation finishes ---ok 

git配置

注册GitHub
访问: http://www.github.com/
注册你的username和邮箱,邮箱十分重要,GitHub上很多通知都是通过邮箱的。
配置SSH keys(我之前配过,so以下内容是别人博客上的,我就不重新来了)
我们如何让本地git项目与远程的github建立联系呢?用SSH keys。
检查SSH keys的设置
1、创建一个 SSH key 

$ ssh-keygen -t rsa -C "your_email@example.com"

代码参数含义:
-t 指定密钥类型,默认是 rsa ,可以省略。-C 设置注释文字,比如邮箱。-f 指定密钥文件存储文件名。
以上代码省略了 -f 参数,因此,运行上面那条命令后会让你输入一个文件名,用于保存刚才生成的 SSH key 代码,如:

Generating public/private rsa key pair.

Enter file in which to save the key (/c/Users/you/.ssh/id_rsa): [Press enter]

当然,你也可以不输入文件名,使用默认文件名(推荐),那么就会生成 id_rsa 和 id_rsa.pub 两个秘钥文件。
 
接着又会提示你输入两次密码(该密码是你push文件的时候要输入的密码,而不是github管理者的密码)

当然,你也可以不输入密码,直接按回车。那么push的时候就不需要输入密码,直接提交到github上了,如:

Enter passphrase (empty for no passphrase): # Enter same passphrase again:

接下来,就会显示如下代码提示,如:

Your identification has been saved in /c/Users/you/.ssh/id_rsa.

Your public key has been saved in /c/Users/you/.ssh/id_rsa.pub.
The key fingerprint is:
01:0f:f4:3b:ca:85:d6:17:a1:7d:f0:68:9d:f0:a2:db your_email @example .com

当你看到上面这段代码的收,那就说明,你的 SSH key 已经创建成功,你只需要添加到github的SSH key上就可以了。
 

2、登陆github系统。点击右上角的 Account Settings—->SSH Public keys —-> add another public keys

3、把你本地生成的密钥复制到里面(key文本框中), 点击 add key 就ok了


测试
可以输入下面的命令,看看设置是否成功,git@github.com的部分不要修改:
$ ssh -T git@github.com

如果是下面的反馈:

The authenticity of host 'github.com (207.97.227.239)' can't be established.RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.Are you sure you want to continue connecting (yes/no)?

不要紧张,输入yes就好,然后会看到:

Hi cnfeat! You've successfully authenticated, but GitHub does not provide shell access.

设置用户信息
现在你已经可以通过SSH链接到GitHub了,还有一些个人信息需要完善的。
Git会根据用户的名字和邮箱来记录提交。GitHub也是用这些信息来做权限的处理,输入下面的代码进行个人信息的设置,把名称和邮箱替换成你自己的,名字必须是你的真名,而不是GitHub的昵称。

$ git config --global user.name "cnfeat"//用户名
$ git config --global user.email "cnfeat@gmail.com"//填写自己的邮箱

SSH Key配置成功
本机已成功连接到github。
若有问题,请重新设置。常见错误请参考:
GitHub Help - Generating SSH Keys
GitHub Help - Error Permission denied (publickey)

github上建立仓库
登录后系统,在github首页,点击页面右下角「New Repository」

Paste_Image.png

填写项目信息:

Paste_Image.png

修改这里的_config.yml
在这里修改_config.yml
添加你的github地址
Paste_Image.png

主题配置(我用的是 Maupassant

hexo主题推荐
Paste_Image.png

站点配置

首先提醒一下

冒号后面有空格

ok 完成,去写blog吧

命令总结

hexo new "postName" #新建文章
hexo new page "pageName" #新建页面
hexo generate #生成静态页面至public目录
hexo server #开启预览访问端口(默认端口4000,'ctrl + c'关闭server)
hexo deploy #将.deploy目录部署到GitHub
hexo help # 查看帮助
hexo version #查看Hexo的版本

复合命令

hexo deploy -g #生成加部署
hexo server -g#生成加预览

命令的简写为

hexo n == hexo new
hexo g == hexo generate
hexo s == hexo server
hexo d == hexo deploy

//尴尬的是 最近重装系统 把本地文件丢失了 blog文章丢失 幸好还有 简书
最后附上我的blog地址 外城

Java 开源博客 Solo 1.9.0 发布 - 新皮肤

$
0
0

这个版本主要是改进了评论模版机制,让大家更方便皮肤制作,并发布了一款新皮肤:9IPHP。

Solo 是一款 一个命令就能搭建好的 Java 开源博客系统,并内置了 15+ 套精心制作的皮肤。除此之外,Solo 还有着非常活跃的 社区,文章分享到社区后可以让很多人看到,产生丰富的交流互动。

项目地址:

近实时搜索SearcherManager和NRTManager的使用 - 学习笔记 - 博客频道 - CSDN.NET

$
0
0

lucene通过NRTManager这个类来实现近实时搜索,所谓近实时搜索即在索引发生改变时,通

过线程跟踪,在相对很短的时间反映给给用户程序的调用

NRTManager通过管理IndexWriter对象,并将IndexWriter的一些方法(增删改)例如

addDocument,deleteDocument等方法暴露给客户调用,它的操作全部在内存里面,所以如果

你不调用IndexWriter的commit方法,通过以上的操作,用户硬盘里面的索引库是不会变化的,所

以你每次更新完索引库请记得commit掉,这样才能将变化的索引一起写到硬盘中,实现索引更新后的同步

用户每次获取最新索引(IndexSearcher),可以通过两种方式,第一种是通过调用

NRTManagerReopenThread对象,该线程负责实时跟踪索引内存的变化,每次变化就调用

maybeReopen方法,保持最新代索引,打开一个新的IndexSearcher对象,而用户所要的

IndexSearcher对象是NRTManager通过调用getSearcherManager方法获得SearcherManager对

象,然后通过SearcherManager对象获取IndexSearcher对象返回个客户使用,用户使用完之

后调用SearcherManager的release释放IndexSearcher对象,最后记得关闭NRTManagerReopenThread;

第二种方式是不通过NRTManagerReopenThread对象,而是直接调用NRTManager的

maybeReopen方法来获取最新的IndexSearcher对象来获取最新索引

1、工程目录



2、只使用SearcherManager,不使用NRTManager的方式搜索

package org.itat.index;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.Executors;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.SearcherWarmer;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.Version;
/**
 * 只使用SearcherManager,不使用NRTManager
 * 
 * SearcherManager的maybeReopen会自动检查是否需要重新打开,比如重复执行search02几次,
 * 中间的一次删除一条数据这个删除的数据需要对writer进行commit才行,这样硬盘上的索引才会生效
 * 那么使用maybeReopen就可以检测到硬盘中的索引是否改变,并在下次查询的时候就进行生效
 * 
 * 但是:
 * 光使用SearcherManager的话做不到实时搜索,为什么呢?
 * 因为使用SearcherManager需要进行writer.commit才会检测到,但是我们知道writer的commit是非常
 * 消耗性能的,我们不能经常性的commit,那需要怎么做呢?
 * 我们只能把添加修改删除的操作在内存中生效,然后使用内存中的索引信息并且在搜索时能起到效果,
 * 过一段时间累计到一定程序才进行writer.commit
 */
public class IndexUtil1 {
	private String[] ids = {"1","2","3","4","5","6"};
	private String[] emails = {"aa@itat.org","bb@itat.org","cc@cc.org","dd@sina.org","ee@zttc.edu","ff@itat.org"};
	private String[] contents = {
			"welcome to visited the space,I like book",
			"hello boy, I like pingpeng ball",
			"my name is cc I like game",
			"I like football",
			"I like football and I like basketball too",
			"I like movie and swim"
	};
	private int[] attachs = {2,3,1,4,5,5};
	private String[] names = {"zhangsan","lisi","john","jetty","mike","jake"};
	private Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_35);
	private SearcherManager mgr = null;//是线程安全的
	private Directory directory = null;
	public IndexUtil1() {
		try {
			directory = FSDirectory.open(new File("D:\\Workspaces\\realtime\\index"));
			mgr = new SearcherManager(
				directory, 
				new SearcherWarmer() {
				/**
				 * 索引一更新就要重新获取searcher,那获取searcher的时候就会调用这个方法
				 * 执行maybeReopen的时候会执行warm方法,在这里可以对资源等进行控制
				 */
					@Override
					public void warm(IndexSearcher search) throws IOException {
						System.out.println("has change");
					}
				}, 
				Executors.newCachedThreadPool()
			);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * 删除索引数据,默认不会完全删除,被放入索引回收站
	 */
	public void delete(String id) {
		IndexWriter writer = null;
		try {
			writer = new IndexWriter(directory,
					new IndexWriterConfig(Version.LUCENE_35,analyzer));
			//参数是一个选项,可以是一个Query,也可以是一个term,term是一个精确查找的值
			//此时删除的文档并不会被完全删除,而是存储在一个回收站中的,可以恢复
			//执行完这个操作,索引文件夹下就会多出一个名叫_0_1.del的文件,也就是删除的文件在这个文件中记录了
			writer.deleteDocuments(new Term("id",id));
			writer.commit();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (LockObtainFailedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(writer!=null) writer.close();
			} catch (CorruptIndexException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	public void query() {
		try {
			IndexReader reader = IndexReader.open(directory);
			//通过reader可以有效的获取到文档的数量
			System.out.println("numDocs:"+reader.numDocs());//存储的文档数//不包括被删除的
			System.out.println("maxDocs:"+reader.maxDoc());//总存储量,包括在回收站中的索引
			System.out.println("deleteDocs:"+reader.numDeletedDocs());
			reader.close();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 *	索引文件后缀为.fmn为保存的是域的名称等
	 * .fdt和.fdx保存的是Store.YES的信息,保存域里面存储的数据
	 * .frq表示这里的域哪些出现多少次,哪些单词出现多少次,
	 * .nrm存储一些评分信息
	 * .prx存储一些偏移量等
	 * .tii和.tis专门存储索引里面的所有内容信息
	 */
	public void index() {
		IndexWriter writer = null;
		try {
			//在2.9版本之后,lucene的就不是全部的索引格式都兼容的了,所以在使用的时候必须写明版本号
			writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_35, analyzer));
			writer.deleteAll();//清空索引
			Document doc = null;
			for(int i=0;i<ids.length;i++) {
				doc = new Document();
				doc.add(new Field("id",ids[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
				doc.add(new Field("email",emails[i],Field.Store.YES,Field.Index.NOT_ANALYZED));
				doc.add(new Field("email","test"+i+"@test.com",Field.Store.YES,Field.Index.NOT_ANALYZED));
				doc.add(new Field("content",contents[i],Field.Store.NO,Field.Index.ANALYZED));
				doc.add(new Field("name",names[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
				//存储数字
				//NumberTools.stringToLong("");已经被标记为过时了
				doc.add(new NumericField("attach",Field.Store.YES,true).setIntValue(attachs[i]));
				String et = emails[i].substring(emails[i].lastIndexOf("@")+1);
				writer.addDocument(doc);
			}
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (LockObtainFailedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(writer!=null)writer.close();
			} catch (CorruptIndexException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	public void search02() {
		IndexSearcher searcher = mgr.acquire();//获得一个searcher
		try {
			/**
			 * maybeReopen会自动检查是否需要重新打开
			 * 比如重复执行search02几次,中间一次删除一条数据
			 * 这个删除的数据需要对writer进行commit才行
			 * 那么使用maybeReopen就可以检测到硬盘中的索引是否改变
			 * 并在下次查询的时候把删除的这条给去掉
			 */
			mgr.maybeReopen();
			TermQuery query = new TermQuery(new Term("content","like"));
			TopDocs tds = searcher.search(query, 10);
			for(ScoreDoc sd:tds.scoreDocs) {
				Document doc = searcher.doc(sd.doc);
				System.out.println(doc.get("id")+"---->"+
						doc.get("name")+"["+doc.get("email")+"]-->"+doc.get("id")+","+
						doc.get("attach")+","+doc.get("date")+","+doc.getValues("email")[1]);
			}
			searcher.close();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally{
			try {
				mgr.release(searcher);//释放一个searcher
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}


3、SearcherManager和NRTManager联合使用

package org.itat.index;
import java.io.File;
import java.io.IOException;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.NRTManager;
import org.apache.lucene.search.NRTManagerReopenThread;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.SearcherManager;
import org.apache.lucene.search.SearcherWarmer;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.Version;
/**
 * SearcherManager和NRTManager联合使用
 * 
 * SearcherManager的maybeReopen会自动检查是否需要重新打开,比如重复执行search02几次,
 * 中间的一次删除一条数据这个删除的数据需要对writer进行commit才行,这样硬盘上的索引才会生效
 * 那么使用maybeReopen就可以检测到硬盘中的索引是否改变,并在下次查询的时候就进行生效
 * 
 * 但是:
 * 光使用SearcherManager的话做不到实时搜索,为什么呢?
 * 因为使用SearcherManager需要进行writer.commit才会检测到,但是我们知道writer的commit是非常
 * 消耗性能的,我们不能经常性的commit,那需要怎么做呢?
 * 我们只能把添加修改删除的操作在内存中生效,然后使用内存中的索引信息并且在搜索时能起到效果,
 * 过一段时间累计到一定程序才进行writer.commit
 * NRTManage就是这样的功能,把更新的数据存储在内容中,但是lucene搜索的时候也可以搜索到,需要
 * writer进行commit才会把索引更新到硬盘中
 */
public class IndexUtil2 {
	private String[] ids = {"1","2","3","4","5","6"};
	private String[] emails = {"aa@itat.org","bb@itat.org","cc@cc.org","dd@sina.org","ee@zttc.edu","ff@itat.org"};
	private String[] contents = {
			"welcome to visited the space,I like book",
			"hello boy, I like pingpeng ball",
			"my name is cc I like game",
			"I like football",
			"I like football and I like basketball too",
			"I like movie and swim"
	};
	private int[] attachs = {2,3,1,4,5,5};
	private String[] names = {"zhangsan","lisi","john","jetty","mike","jake"};
	private Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_35);
	private SearcherManager mgr = null;//是线程安全的
	private NRTManager nrtMgr = null;//near real time  近实时搜索
	private Directory directory = null;
	private IndexWriter writer = null;
	public IndexUtil2() {
		try {
			directory = FSDirectory.open(new File("D:\\Workspaces\\realtime\\index"));
			writer = new IndexWriter(directory,new IndexWriterConfig(Version.LUCENE_35, analyzer));
			nrtMgr = new NRTManager(writer, 
						new SearcherWarmer() {
							/**
							 * 索引一更新就要重新获取searcher,那获取searcher的时候就会调用这个方法
							 * 执行maybeReopen的时候会执行warm方法,在这里可以对资源等进行控制
							 */
							@Override
							public void warm(IndexSearcher search) throws IOException {
								System.out.println("has open");
							}
						}
			);
			//启动NRTManager的Reopen线程
			//NRTManagerReopenThread会每隔25秒去检测一下索引是否更新并判断是否需要重新打开writer
			NRTManagerReopenThread reopen = new NRTManagerReopenThread(nrtMgr, 5.0, 0.025);//0.025为25秒
			reopen.setDaemon(true);//设为后台线程
			reopen.setName("NrtManager Reopen Thread");
			reopen.start();
			mgr = nrtMgr.getSearcherManager(true);//true为允许所有的更新
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * 删除索引数据,默认不会完全删除,被放入索引回收站
	 */
	public void delete(String id) {
		try {
			//参数是一个选项,可以是一个Query,也可以是一个term,term是一个精确查找的值
			//此时删除的文档并不会被完全删除,而是存储在一个回收站中的,可以恢复
			//执行完这个操作,索引文件夹下就会多出一个名叫_0_1.del的文件,也就是删除的文件在这个文件中记录了
			nrtMgr.deleteDocuments(new Term("id",id));//使用使用nrtMgr来删除
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (LockObtainFailedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} 
	}
	public void query() {
		try {
			IndexReader reader = IndexReader.open(directory);
			//通过reader可以有效的获取到文档的数量
			System.out.println("numDocs:"+reader.numDocs());//存储的文档数//不包括被删除的
			System.out.println("maxDocs:"+reader.maxDoc());//总存储量,包括在回收站中的索引
			System.out.println("deleteDocs:"+reader.numDeletedDocs());
			reader.close();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 *	索引文件后缀为.fmn为保存的是域的名称等
	 * .fdt和.fdx保存的是Store.YES的信息,保存域里面存储的数据
	 * .frq表示这里的域哪些出现多少次,哪些单词出现多少次,
	 * .nrm存储一些评分信息
	 * .prx存储一些偏移量等
	 * .tii和.tis专门存储索引里面的所有内容信息
	 */
	public void index() {
		IndexWriter writer = null;
		try {
			//在2.9版本之后,lucene的就不是全部的索引格式都兼容的了,所以在使用的时候必须写明版本号
			writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_35, analyzer));
			writer.deleteAll();//清空索引
			Document doc = null;
			for(int i=0;i<ids.length;i++) {
				doc = new Document();
				doc.add(new Field("id",ids[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
				doc.add(new Field("email",emails[i],Field.Store.YES,Field.Index.NOT_ANALYZED));
				doc.add(new Field("email","test"+i+"@test.com",Field.Store.YES,Field.Index.NOT_ANALYZED));
				doc.add(new Field("content",contents[i],Field.Store.NO,Field.Index.ANALYZED));
				doc.add(new Field("name",names[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
				//存储数字
				//NumberTools.stringToLong("");已经被标记为过时了
				doc.add(new NumericField("attach",Field.Store.YES,true).setIntValue(attachs[i]));
				String et = emails[i].substring(emails[i].lastIndexOf("@")+1);
				writer.addDocument(doc);
			}
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (LockObtainFailedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(writer!=null)writer.close();
			} catch (CorruptIndexException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	public void search02() {
		IndexSearcher searcher = mgr.acquire();//获得一个searcher
		try {
			/**
			 * maybeReopen会自动检查是否需要重新打开
			 * 比如重复执行search02几次,中间一次删除一条数据
			 * 这个删除的数据需要对writer进行commit才行
			 * 那么使用maybeReopen就可以检测到硬盘中的索引是否改变
			 * 并在下次查询的时候把删除的这条给去掉
			 */
			TermQuery query = new TermQuery(new Term("content","like"));
			TopDocs tds = searcher.search(query, 10);
			for(ScoreDoc sd:tds.scoreDocs) {
				Document doc = searcher.doc(sd.doc);
				System.out.println(doc.get("id")+"---->"+
						doc.get("name")+"["+doc.get("email")+"]-->"+doc.get("id")+","+
						doc.get("attach")+","+doc.get("date")+","+doc.getValues("email")[1]);
			}
			searcher.close();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally{
			try {
				mgr.release(searcher);//释放一个searcher
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

4、不使用SearcherManager和NRTManager

package org.itat.index;
import java.io.File;
import java.io.IOException;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericField;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.Version;
/**
 * 不使用SearcherManager和NRTManager
 * @author user
 */
public class IndexUtil {
	private String[] ids = {"1","2","3","4","5","6"};
	private String[] emails = {"aa@itat.org","bb@itat.org","cc@cc.org","dd@sina.org","ee@zttc.edu","ff@itat.org"};
	private String[] contents = {
			"welcome to visited the space,I like book",
			"hello boy, I like pingpeng ball",
			"my name is cc I like game",
			"I like football",
			"I like football and I like basketball too",
			"I like movie and swim"
	};
	private int[] attachs = {2,3,1,4,5,5};
	private String[] names = {"zhangsan","lisi","john","jetty","mike","jake"};
	private Directory directory = null;
	private static IndexReader reader = null;
	public IndexUtil() {
		try {
			directory = FSDirectory.open(new File("D:\\Workspaces\\realtime\\index"));
//			directory = new RAMDirectory();
//			index();
			reader = IndexReader.open(directory,false);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 * 对于IndexReader而言,反复使用Index.open打开会有很大的开销,所以一般在整个程序的生命周期中
	 * 只会打开一个IndexReader,通过这个IndexReader来创建不同的IndexSearcher,如果使用单例模式,
	 * 可能出现的问题有:
	 * 1、当使用Writer修改了索引之后不会更新信息,所以需要使用IndexReader.openIfChange方法操作
	 * 如果IndexWriter在创建完成之后,没有关闭,需要进行commit操作之后才能提交
	 * @return
	 */
	public IndexSearcher getSearcher() {
		try {
			if(reader==null) {
				reader = IndexReader.open(directory,false);
			} else {
				IndexReader tr = IndexReader.openIfChanged(reader);
				//如果原来的reader没改变,返回null
				//如果原来的reader改变,则更新为新的索引
				if(tr!=null) {
					reader.close();
					reader = tr;
				}
			}
			return new IndexSearcher(reader);
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return null;
	}
	/**
	 * 删除索引数据,默认不会完全删除,被放入索引回收站
	 */
	public void delete() {
		IndexWriter writer = null;
		try {
			writer = new IndexWriter(directory,
					new IndexWriterConfig(Version.LUCENE_35,new StandardAnalyzer(Version.LUCENE_35)));
			//参数是一个选项,可以是一个Query,也可以是一个term,term是一个精确查找的值
			//此时删除的文档并不会被完全删除,而是存储在一个回收站中的,可以恢复
			//执行完这个操作,索引文件夹下就会多出一个名叫_0_1.del的文件,也就是删除的文件在这个文件中记录了
			writer.deleteDocuments(new Term("id","1"));
			writer.commit();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (LockObtainFailedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(writer!=null) writer.close();
			} catch (CorruptIndexException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	public void query() {
		try {
			IndexReader reader = IndexReader.open(directory);
			//通过reader可以有效的获取到文档的数量
			System.out.println("numDocs:"+reader.numDocs());//存储的文档数//不包括被删除的
			System.out.println("maxDocs:"+reader.maxDoc());//总存储量,包括在回收站中的索引
			System.out.println("deleteDocs:"+reader.numDeletedDocs());
			reader.close();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	/**
	 *	索引文件后缀为.fmn为保存的是域的名称等
	 * .fdt和.fdx保存的是Store.YES的信息,保存域里面存储的数据
	 * .frq表示这里的域哪些出现多少次,哪些单词出现多少次,
	 * .nrm存储一些评分信息
	 * .prx存储一些偏移量等
	 * .tii和.tis专门存储索引里面的所有内容信息
	 */
	public void index() {
		IndexWriter writer = null;
		try {
			//在2.9版本之后,lucene的就不是全部的索引格式都兼容的了,所以在使用的时候必须写明版本号
			writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_35, new StandardAnalyzer(Version.LUCENE_35)));
			writer.deleteAll();//清空索引
			Document doc = null;
			for(int i=0;i<ids.length;i++) {
				doc = new Document();
				doc.add(new Field("id",ids[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
				doc.add(new Field("email",emails[i],Field.Store.YES,Field.Index.NOT_ANALYZED));
				doc.add(new Field("email","test"+i+"@test.com",Field.Store.YES,Field.Index.NOT_ANALYZED));
				doc.add(new Field("content",contents[i],Field.Store.NO,Field.Index.ANALYZED));
				doc.add(new Field("name",names[i],Field.Store.YES,Field.Index.NOT_ANALYZED_NO_NORMS));
				//存储数字
				//NumberTools.stringToLong("");已经被标记为过时了
				doc.add(new NumericField("attach",Field.Store.YES,true).setIntValue(attachs[i]));
				String et = emails[i].substring(emails[i].lastIndexOf("@")+1);
				writer.addDocument(doc);
			}
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (LockObtainFailedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(writer!=null)writer.close();
			} catch (CorruptIndexException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	public void search02() {
		try {
			IndexSearcher searcher = getSearcher();
			TermQuery query = new TermQuery(new Term("content","like"));
			TopDocs tds = searcher.search(query, 10);
			for(ScoreDoc sd:tds.scoreDocs) {
				Document doc = searcher.doc(sd.doc);
				System.out.println(doc.get("id")+"---->"+
						doc.get("name")+"["+doc.get("email")+"]-->"+doc.get("id")+","+
						doc.get("attach")+","+doc.get("date")+","+doc.getValues("email")[1]);
			}
			searcher.close();
		} catch (CorruptIndexException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}


工程路径:http://download.csdn.net/detail/wxwzy738/5332972



[Elasticsearch] 控制相关度 (五) - function_score查询及field_value_factor,boost_mode,max_mode参数 - dm_vincent的专栏 - 博客频道 - CSDN.NET

$
0
0

本章翻译自Elasticsearch官方指南的Controlling Relevance一章。



function_score查询

function_score查询是处理分值计算过程的终极工具。它让你能够对所有匹配了主查询的每份文档调用一个函数来调整甚至是完全替换原来的_score。

实际上,你可以通过设置过滤器来将查询得到的结果分成若干个子集,然后对每个子集使用不同的函数。这样你就能够同时得益于:高效的分值计算以及可缓存的过滤器。

它拥有几种预先定义好了的函数:

weight

对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。

field_value_factor

使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。

random_score

使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。

衰减函数(Decay Function) - linear,exp,gauss

将像publish_date,geo_location或者price这类浮动值考虑到_score中,偏好最近发布的文档,邻近于某个地理位置(译注:其中的某个字段)的文档或者价格(译注:其中的某个字段)靠近某一点的文档。

script_score

使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。

没有function_score查询的话,我们也许就不能将全文搜索得到分值和近因进行结合了。我们将不得不根据_score或者date进行排序;无论采用哪一种都会抹去另一种的影响。function_score查询让我们能够将两者融合在一起:仍然通过全文相关度排序,但是给新近发布的文档,或者流行的文档,或者符合用户价格期望的文档额外的权重。你可以想象,一个拥有所有这些功能的查询看起来会相当复杂。我们从一个简单的例子开始,循序渐进地对它进行介绍。



根据人气来提升(Boosting by Popularity)

假设我们有一个博客网站让用户投票选择他们喜欢的文章。我们希望让人气高的文章出现在结果列表的头部,但是主要的排序依据仍然是全文搜索分值。我们可以通过保存每篇文章的投票数量来实现:

PUT /blogposts/post/1{"title":"About popularity","content":"In this post we will talk about...","votes":6}

在搜索期间,使用带有field_value_factor函数的function_score查询将投票数和全文相关度分值结合起来:

GET /blogposts/post/_search
{"query": {"function_score": {"query": {"multi_match": {"query":"popularity","fields": ["title","content"]
        }
      },"field_value_factor": {"field":"votes"}
    }
  }
}

function_score查询会包含主查询(Main Query)和希望适用的函数。先会执行主查询,然后再为匹配的文档调用相应的函数。每份文档中都必须有一个votes字段用来保证function_score能够起作用。

在前面的例子中,每份文档的最终_score会通过下面的方式改变:

new_score = old_score * number_of_votes

它得到的结果并不好。全文搜索的_score通常会在0到10之间。而从下图我们可以发现,拥有10票的文章的分值大大超过了这个范围,而没有被投票的文章的分值会被重置为0。

modifier

为了让votes值对最终分值的影响更缓和,我们可以使用modifier。换言之,我们需要让头几票的效果更明显,其后的票的影响逐渐减小。0票和1票的区别应该比10票和11票的区别要大的多。

一个用于此场景的典型modifier是log1p,它将公式改成这样:

new_score = old_score * log(1 + number_of_votes)

log函数将votes字段的效果减缓了,其效果类似下面的曲线:

使用了modifier参数的请求如下:

GET /blogposts/post/_search
{"query": {"function_score": {"query": {"multi_match": {"query":"popularity","fields": ["title","content"]
        }
      },"field_value_factor": {"field":"votes","modifier":"log1p"}
    }
  }
}

可用的modifiers有:none(默认值),log,log1p,log2p,ln,ln1p,ln2p,square,sqrt以及reciprocal。它们的详细功能和用法可以参考field_value_factor文档

factor

可以通过将votes字段的值乘以某个数值来增加该字段的影响力,这个数值被称为factor:

GET /blogposts/post/_search
{"query": {"function_score": {"query": {"multi_match": {"query":"popularity","fields": ["title","content"]
        }
      },"field_value_factor": {"field":"votes","modifier":"log1p","factor":2}
    }
  }
}

添加了factor将公式修改成这样:

new_score = old_score * log(1 + factor * number_of_votes)

当factor大于1时,会增加其影响力,而小于1的factor则相应减小了其影响力,如下图所示:

boost_mode

将全文搜索的相关度分值乘以field_value_factor函数的结果,对最终分值的影响可能太大了。通过boost_mode参数,我们可以控制函数的结果应该如何与_score结合在一起,该参数接受下面的值:

  • multiply:_score乘以函数结果(默认情况)
  • sum:_score加上函数结果
  • min:_score和函数结果的较小值
  • max:_score和函数结果的较大值
  • replace:将_score替换成函数结果

如果我们是通过将函数结果累加来得到_score,其影响会小的多,特别是当我们使用了一个较低的factor时:

GET /blogposts/post/_search
{"query": {"function_score": {"query": {"multi_match": {"query":"popularity","fields": ["title","content"]
        }
      },"field_value_factor": {"field":"votes","modifier":"log1p","factor":0.1},"boost_mode":"sum"}
  }
}

上述请求的公式如下所示:

new_score = old_score + log(1 + 0.1 * number_of_votes)

max_boost

最后,我们能够通过制定max_boost参数来限制函数的最大影响:

GET /blogposts/post/_search
{"query": {"function_score": {"query": {"multi_match": {"query":"popularity","fields": ["title","content"]
        }
      },"field_value_factor": {"field":"votes","modifier":"log1p","factor":0.1},"boost_mode":"sum","max_boost":1.5}
  }
}

无论field_value_factor函数的结果是多少,它绝不会大于1.5。

NOTE

max_boost只是对函数的结果有所限制,并不是最终的_score。




我当初是怎么管理技术团队的 - 旁观者 - 博客园

$
0
0

此文为早期文档

创建于2015/6/28

关键词:管理技术人才、管理技术团队、技术传承、对题集/错题集、研发哲学

本文档适用人员:研发干部

阅读大约需要二十分钟。

0x00,背景知识

窝窝技术团队大约两三百人左右,主要是五大块:研发、数据、无线、质量、运维。

2012年年初,一个大项目结束后,我召开了飞行研讨会,经过这次深刻反思,形成了几个影响深远的管理观点:

  1. 管理者要向下提供工具,以形成干部的简单、易记忆、易执行的工作套路。

  2. 干部要直面白刃战,必须随时能深入到业务细节。

  3. 培训直到最基层,有案例点评,结合正反实例反复讲,有机会就讲,有机会就补充,有机会就强调,不断检查,不断重复。业务精英都是从五湖四海汇集而来,工作习惯、方式、话术、意识不尽相同,所以需要通过重复重复再重复、CheckCheck再Check,不仅仅要矫正错误行为,更重要的是指明什么是正确的行为。

随后的数年岁月里,首先我们在实践中形成了自己的研发哲学:

  1. Don't make me think:凡是被很多人不断重复的好习惯,要将其自动化,绑定到工具之中,以"Don't make me think"的方式来推广最佳实践(best practice)。

  2. If it hurts, do it more and often:如果一件事做起来很烦,那就把它拆成很多块儿,每天做一点,每次做一点。

  3. 这个世界从来没有什么救世主:从来就没有什么救世主,要创造人类的幸福全靠我们自己,不要指望有什么人能救我们,只能绞尽脑汁闯阵。

  4. 没有苦劳,只有功劳:没有结果就没有意义。不要期望公司因为你和小伙伴们有苦劳而宽容你们没有产出,这是一个商业公司。

其次,经过长期的磨合,我们认同如下理论:

企业的研发管理应该注重建立一个良性的循环:

  1. 研发能力的提升,有助于促进研发效率的改善;

  2. 研发效率的提升,使得研发人员可以有更多的空余时间,进而激发更多的研发活力;

  3. 研发活力的提升,研发人员积极交流与分享,有助于提升研发人员的总体能力。

过去的软件开发方法论,往往只是注重了研发管理中的一两个方面,缺乏整体视角,常常期望以一套方法论包打天下。事实上,真实情况下的研发管理,需要至少三套方法论:

  1. 提升研发能力,主要依靠经验积累,建立企业内部的知识库与传承体系(促进交流与协作,借助研发活力促进研发能力提升,也很重要);

  2. 提升研发效率,主要依靠科学的数据分析,建立或引进一系列的研发工具,建立合理的流程与制度(通过提升研发人员能力,激发他们不断改进效率,也很重要);

  3. 提升研发活力,主要靠多种社会化的沟通机制,促进分享与交流(给研发人员松绑,让他们有足够的空余时间,也很重要)。

我们从2012年开始推行的很多策略制度都暗合以上理论,下面展开讲一讲。

0x01,管控基本思路

=2012年=

1.1.RCA制度

话说2011年下半年,多个技术团队融合,又处于“飞行过程中换发动机”的重构期间,陆续出现了项目一再 delay、Bug 数迟迟不能收敛、相似问题在不同团队反复发生、刚修复的 Bug 又复现等现象,团队之间也互相抱怨。为了遏制这种势头,我组织了一些项目管理者和大家分享他们之前的成功经验,看看能不能从中找到解决之道。

2011年9月的一次分享会上,鲍嘉宝分享了他在上一家公司做飞信项目时降低 Bug 率的几个措施:

方案一:逐步落实质量保障措施

  1. 增加 RCA(Root Cause Analysis,根本原因分析)制度,对 Bug 的成因进行分析和标注,定时汇总并通告,让开发人员集体增长问题解决经验,减少同类问题多次出现的概率;

  2. 开展协议与接口规范讲解,降低使用方因为理解偏差或者使用随意造成问题的概率;

  3. 强制推行编码规范,降低代码随意造成问题的概率;

  4. 规范化技术方案实施与评审机制,降低逻辑层面与架构层面造成问题的概率;

方案二:Bug 评审会

  1. 定期或不定期开展 Bug 评审会,清理库存 Bug,或者针对难以解决的 Bug 协商处理方案;

  2. 评审会上对“解决 Bug”和“做新需求”的优先级进行明确安排;

  3. Bug 评审会还具备其他职责,包括回归测试出现问题的解决方案讨论,上线前遗留 Bug 的处理措施的确定等。

最终,从2012年9月开始,研发体系正式推广 RCA 制度,双周一次 RCA 总结会,发送质量保障周报,公布每个开发任务的有效软件 Bug 数和千行代码缺陷率等指标。

当时的具体做法如下所示:

1)双周一次RCA总结会

主持人:质量控制负责人

形式:会议

面向对象:研发全体+质量控制全体

时间:每双周五下午14点~16点

开始执行时间:2012 年 9 月 28 日

规则:

1. 点评案例提前准备好,要点名;

2. 重点针对漏测 Bug ;

2.1. 兼顾有典型意义的 Bug;

3. 回顾每一个 RCA 案例时,做到:

3.1. 造成的后果或影响,

3.2. BUG的原因(一定要问清楚,说得太含糊可起不到警示作用),

3.3. 漏测的原因,

3.4. 得到的教训,

3.5. 事后补救措施或改进方案。

2)每周质量保障报告



报告人:质量控制负责人

形式:邮件

发送范围:质量控制部全体,研发部全体,产品经理全体,SA 全体

第一份报告:漏测BUG简报

字段:

项目名称 上线时间 BUG描述 严重程度 发现人 线上处理办法 责任人 缺陷类型

第二份报告:项目质量简报

字段:

项目名称 提测时间 测试用例数 (预计)上线时间 有效Bug数 严重Bug数 严重缺陷率 平均Bug解决时长 千行代码缺陷率

在之后的几年里,我们在技术团队内部贯彻了 RCA 处理流程:

  • 收集足够多证据

  • 重新描述问题

  • 现象没有描述清楚之前,切勿下结论

  • 构建证据链

  • 给出结论

  • 线下重现

  • 确认影响范围

  • 纠正线上数据

  • 制定防范措施

  • 撰写 RCA 报告

  • 公开通报(包括同步给其他业务部门)

  • 纳入 RCA 案例库

时至今日,RCA 已汇总了七季,RCA 报告数百个,成为了研发体系的宝贵财富。

RCA报告的标准格式为:

  1. 背景知识(Optional)

  2. 问题现象

  3. 影响范围

  4. 问题原因

  5. 问题分析过程(Optional)

  6. 解决办法

  7. 后续处理措施:如线上脏数据如何修复,如对用户造成的影响如何弥补等(Optional)

  8. 经验教训

  9. RCA类型:如代码问题、实施问题、配置问题、设计问题、测试问题

=2013年 - 2014年=

在设定2013年工作计划时,我明确需要解决如下三个问题:

  1. 对产品的质量及细节(如稳定性、速度、体验等)的把控不足,

  2. 团队的开发效率不够理想(如产品的迭代速度慢),

  3. 技术团队对于行业关键技术的掌握能力偏弱。

我认识到,必须预留足够多的研发资源,优先解决技术团队日常开发和维护过程中遇到的痛苦。

那时候我们有哪些痛苦?

  1. 开发联调环境、测试环境搭建和部署麻烦,动辄服务中断、接口,开发者为此劳心劳力。

  2. 系统之间的数据同步,一般通过接口同步调用达成,不够可靠,不能保证最终数据一致性,遗漏后难以排查。

  3. 层出不穷的定时任务难以管理,日志难以查看,常常是用户投诉了,我们才发现定时任务没有执行或者执行失败了。

  4. 不能保证平滑上线,常常通宵上线。

……

于是,2013年集中解决制造工具持续集成过程透明化数据化这三件事。

1.2.制造工具

制造(或发现)什么样的工具 ?答案是:

  1. 提高开发效率的 ,

  2. 提高系统可伸缩和可靠性的,

  3. 不同业务线可复用的。

方法是:

  • 找到技术团队的痛点;

  • 找到技术团队的生产效率低的原因;

  • 抽象业务场景;

  • 有针对性地深入了解其他公司如何解决的,梳理各种方案,向功课好的学生学习;

  • 发现现有开源工具,或组织人员开发工具,制定和验证高可用方案 。

实例:

  • 自动化测试

    • 早期的自动化测试都是基于 QTP 的,比较古老。2013年开始,质量控制部一支人马开始基于 Robot Framework(Python)做,另一支人马则基于 Lazyman(Ruby)展开做。

  • 持续集成

    • 2012年大家想做持续集成,之后大家各自发展,一,主站全部切换到 gitlab 管理代码,二,由 hudson 或 jenkins 驱动构建,三,使用统一的 maven 库,四,去除那些影响构建和部署的配置项,五,运维部开发自动化上线的管理平台。

  • 定时任务调度和管理 JobCenter

    • 最开始是因为多个定时任务停了或天天报错,但无人知晓,直到用户投诉。

    • 所以痛下决心整顿。开发中间件 JobCenter 花了三个月,推广又花了三个月。

  • 异步推送 NotifyServer

    • CMS 与商品中心之间的消息推送屡屡失败,引发各种问题和投诉,排查费时费力。

    • 最终决定自行研发一个健壮的中间件 PushServer。

时至今日(注:指2015年),积累的通用技术工具有:

  1. 前期基于StatsD+Graphite,后期基于OpenTSDB的智能监控解决方案-天机

  2. 定时任务调度与管理系统-JobCenter

  3. 内部统一认证系统-IdCenter

  4. 异步消息可靠推送-NotifyServer

  5. 分布式跟踪系统-鹰眼

  6. 分布式缓存管理平台-Discache

  7. 基于ELK的异常日志分析方案

  8. 基于Diamond的持久化配置中心和业务降级方案

  9. 基于ElasticSearch的搜索筛选排序解决方案

  10. 基于FastDFS的文件上传和分布式文件存储系统

  11. 数据库自动化运维平台

  12. 基于Apriori算法的Nginx+Lua+ELK异常流量拦截方案

  13. 基于TCPCopy的仿真压测方案

1.3.持续集成

 我在《窝窝研发过去几年做对了哪些事》一文中讲过:

每逢业务大跃进,就会欠下一屁股技术债。

是债就要还。

酷壳陈浩说过,技术债是不能欠的,要残酷无情地还债。很多事情,一开始不会有,那么就永远不会有。一旦一个事情烂了,后面只能跟着一起烂,烂得越多,就越没有人敢去还债。

首当其冲的就是线下环境和线上环境的维护和发布,大把大把的时间浪费在这上面,一代又一代的工程师在离职时表达了愤懑情绪。

于是,从2012年6月开始到2013年10月底,窝窝研发体系开始了持续集成第一季整改,万事开头难,这一干就是一年多。

所谓的第二季 CI,截止到2014年6月,至此,团购业务线基本做到了线下自动化编译、部署、测试(日检),线上自动化上线和回退。

第三季 CI 主要是让 PHP 工程(CMS和SWP)和前端(CSS/JS/Images)也接入自动化上线体系,截止到2014年10月底,基本完成。

目前,基于 Docker 的私有云深刻地影响着持续集成的方方面面,所有的环节都被改变了。

1.4.过程透明化数据化

2013年我在内部做职场培训时,以《职场(潜)规则:心法和技法》为题讲过:

十三)解释成本很高

彪悍的人生也必须解释。

前面说过,多数人都不太清楚其他板块和部门在做什么,是怎么运转的,管理方式是什么,人都投在哪儿了,为什么做这件事为什么不做那件事,那个外部投诉是怎么解决的,为什么一个我认为简单的问题你们却迟迟未解决。

如果我们没有做到冰箱陈列式的项目管理方式,如果没有养成信息第一时间同步、通报的习惯,我们就可能被迫事后解释。

当你需要解释的时候,其实已经有点儿晚了。

别人心中可能已经形成了观点,可能还传播出去了,你又保证不了你的解释能让该听到的人都听到,听到也不见得再帮你传播。

还会耗费你我大量时间解释,与其如此,不如提前主动通报、制定流程和信息同步。

所以我们应该有意识地遵循如下模式:

模式75 冰箱门

——团队成员定期地把各自的工作成果展现给团队所有的人。



项目产物的公开展示反映出团队成员之间的信任,它传达了一种信号——没有什么会仅仅因为主观原因而隐藏起来。没有人会因为让其他人看到了未完成的事务或进度延迟而担心。冰箱门型项目上的团队成员基本上不会去“偏袒”或者粉饰自己的进度报告。

 

所以,从2013年3月开始,我们试图一步步做到过程数据化 ,然后做到过程透明化:

  • 按项目:

    • 收集项目实施中的各种数据

      • 资源投入、占用和释放

      • 工时

      • 加班工时

      • 代码统计信息

      • 测试用例数

      • BUG数/严重BUG率/缺陷类型/解决时长

      • 线上漏测数

      • 千行代码缺陷率

  • 按部门

    • 细分到日的人员工作饱和度

    • 技术分享和培训的类型和工时

  • 过程透明化

    • 每一个流转环节都向外部干系人通告,过程透明,数据可检索

    • 提前发出延期预警通告

    • 提前发出风险警示

1.5.预研药不能停

工程师文化中有一条:愿意投资比较长期的项目。是的,如果一个技术团队总被”紧急且重要“和”紧急且不重要“的事情牵着鼻子走,没有余力去做”重要但不紧急“的事情,最后一定是被动挨打,积劳成疾,最后病入膏肓。

我在《窝窝研发过去几年做对了哪些事》一文中讲过:

职场潜规则里我讲过,一定要低头拉车抬头看路

最开始我们怎么抬头看路呢,那就是看淘宝这么多年都怎么过来的,他们在不同阶段都在解决什么问题。

冯仑说过,我们要把别人的历史当作自己的未来,这样,才能知道过去人家在做什么,我们现在应该怎么做。

于是,从2013年开始,我们抽调宝贵的研发人力,花费三四个月去做 JobCenter、NotifyServer、鹰眼、数据仓库,花费大量精力去测试 Dubbo、Cobar,做完这些还需要见缝插针地分批分期内部推广。

但这些提前量都是值得做的,否则我们很可能做着做着就“死做做死”了。

 

所以,药不能停,技术领袖需要眼光放长远,技术积累和传承不可能一蹴而就,中间的坑一个也少不了,不趟怎么知道有多少雷,曾鸣的话怎么说的:

什么叫战略?

——做对了一定会有好结果。

什么叫执行?

——中间的苦,一步也少不了。

时至今日(注:指2015年),我们提前预研了这些解决方案:

  1. 基于mesos+marathon+consul+registrator+haproxy+docker的容器虚拟化方案

  2. 基于Shib+Presto的大数据即席查询方案

  3. 基于HUE+Oozie的Hadoop集群调度与管理

  4. 基于Piwik+Flume+Kafka+Storm的推荐评测系统

  5. 基于Cobar的水平分库方案

  6. Disruptor在订单交易中的应用方案

  7. 基于Ionic+Cordova+Saas+Bower+Angularjs+CoffeeScript+Gulp的HTML5移动开发方案

  8. 基于ArtTemplate+FrozenUI+Backbone+Zepto的H5轻应用方案

  9. 灰度发布平台

  10. 运维自动化平台

  11. 自助式报表解决方案

1.6.分享与学习的氛围

 工程师文化中还有两条:分享与学习的氛围强,让工程师有一定的冗余时间自我学习新知识。这也暗合最前面我提到的研发能力、研发效率和研发活力三者之间的循环促进关系。

为了激发研发活力,需要多管齐下,从2012年开始我们有意识去做:

  • 定期举办技术分享讲座,当研发人员足够多,方向足够广时,Topic 还是有很多的;

  • 为了开讲座,需要给主讲人预留出一定的学习和准备时间;

  • 为了让大家有研究有思考有实践,不能把人全陷在具体业务逻辑开发上,不要过分追求低费率和性价比,明明10个人的活儿非让5个人干,最后项目也屡屡延期,一年到头技术团队也没进步。

其次,研发工程师要能够清晰表达。别人听不懂,多半是因为你讲不清楚,你讲不清楚,往往是因为你本来就没想明白没听懂,自然也就没逻辑。

所以,我们从新员工入职之后就有意识要求他们,在试用期内,新人必须做一次技术分享和一次技术评审,面对各方的 challenge,逼着大家学会公开陈述和清晰表达。

以后,我打算实行讲师制度,效仿微博的新兵训练营,申请预算,为训练营讲师的课件制作和授课支付费用。

1.7.组织架构随需调整

当组织结构影响效率时,就要动动组织结构了 ,干部都得抬抬屁股动动窝了。

首要目的是要避免以部门为沟壑。何谓沟壑?阻碍了问题排查或解决,阻碍了技术方案的推行,某类型工程师过少形成管理死角或不利于技术进步,这都叫沟壑。

有时也可能为新业务线提供骨干和人员。这都需要调整部门结构。

以上这七点就是窝窝技术团队在2012、2013、2014这三年间所做出的管控策略。我们认为管理技术人才是一门学问,第一,外行不可能领导内行,第二,靠挖人,靠猎头,一朝一夕之间不可能组建一支能打硬仗的技术团队,那只能攒出乌合之众。

-第一节完-

头图图片来源于必应搜索

欢迎订阅我的微信订阅号『老兵笔记』

 

用jpinyin实现汉字转拼音功能 - developer_Kale - 博客园

$
0
0

 

一、简介

项目地址:https://github.com/stuxuhai/jpinyin

JPinyin是一个汉字转拼音的Java开源类库,在PinYin4j的功能基础上做了一些改进。

【JPinyin主要特性】

1、准确、完善的字库;

Unicode编码从4E00-9FA5范围及3007(〇)的20903个汉字中,JPinyin能转换除46个异体字(异体字不存在标准拼音)之外的所有汉字;

2、拼音转换速度快;

经测试,转换Unicode编码从4E00-9FA5范围的20902个汉字,JPinyin耗时约100毫秒。

3、多拼音格式输出支持;

JPinyin支持多种拼音输出格式:带音标、不带音标、数字表示音标以及拼音首字母输出格式;

4、常见多音字识别;

JPinyin支持常见多音字的识别,其中包括词组、成语、地名等;

5、简繁体中文转换

 

Jpinyin里面一共有四个类:

  • ChineseHelper.java     汉字简繁体转换类
  • PinyinFormat.java         拼音格式类
  • PinyinHelper.java          汉字转拼音类
  • PinyinResource.java    资源文件加载类

 

二、主要方法介绍

2.1 convertToPinyinString(Stringstr,Stringseparator)

/*** 将字符串转换成相应格式的拼音
 *@paramstr 需要转换的字符串
 *@paramseparator 拼音分隔符
 *@return字符串的拼音*/publicstaticString convertToPinyinString(String str, String separator, PinyinFormat pinyinFormat)

结果:

      String words = "和气生财";finalString separator = " ";//hé qì shēng cái (默认格式)PinyinHelper.convertToPinyinString(words, separator);

 

2.2 convertToPinyinString(String str, String separator, PinyinFormat pinyinFormat)

/**

 * 将字符串转换成相应格式的拼音

 * @param str 需要转换的字符串

 * @param separator 拼音分隔符

 * @param pinyinFormat 拼音格式:WITH_TONE_NUMBER--数字代表声调,WITHOUT_TONE--不带声调,WITH_TONE_MARK--带声调

 * @return 字符串的拼音

 */

public static String convertToPinyinString(String str, String separator, PinyinFormat pinyinFormat)

结果:

   String str = "你好世界";

PinyinHelper.convertToPinyinString(str,
",", PinyinFormat.WITH_TONE_MARK);//nǐ,hǎo,shì,jièPinyinHelper.convertToPinyinString(str, ",", PinyinFormat.WITH_TONE_NUMBER);//ni3,hao3,shi4,jie4PinyinHelper.convertToPinyinString(str, ",", PinyinFormat.WITHOUT_TONE);//ni,hao,shi,jie

 

2.3 getShortPinyin(String str)

/*** 获取字符串对应拼音的首字母
 *@paramstr 需要转换的字符串
 *@return对应拼音的首字母*/publicstaticString getShortPinyin(String str)

结果:

String str = "你好世界";PinyinHelper.getShortPinyin(str);//nhsj

 

2.4 convertToPinyinArray(char c)
/*** 将单个汉字转换为相应格式的拼音
 *@paramc 需要转换成拼音的汉字
 *@return汉字的拼音*/publicstaticString[] convertToPinyinArray(charc)

结果:

String words = "和气生财";


//
hé hè huó huò húpinyins = PinyinHelper.convertToPinyinArray(words.toCharArray()[0]);

 

2.5 convertToPinyinArray(char c,PinyinFormatpinyinFormat)

/*** 将单个汉字转换为相应格式的拼音
 *@paramc 需要转换成拼音的汉字
 *@parampinyinFormat 拼音格式:WITH_TONE_NUMBER--数字代表声调,WITHOUT_TONE--不带声调,WITH_TONE_MARK--带声调
 *@return汉字的拼音*/publicstaticString[] convertToPinyinArray(charc, PinyinFormat pinyinFormat)

结果:

String words = "和气生财";


//
hé hè huó huò húpinyins = PinyinHelper.convertToPinyinArray(words.toCharArray()[0], PinyinFormat.WITH_TONE_MARK);

 

2.6 hasMultiPinyin(char c)

/*** 判断一个汉字是否为多音字
 *@paramc 汉字
 *@return判断结果,如果是返回true,否则返回false*/publicstaticbooleanhasMultiPinyin(charc)

结果:

 //falseprintln(PinyinHelper.hasMultiPinyin('李'));

 

 

源码下载:

 

大部分内容参考自:http://blog.csdn.net/ekeuy/article/details/40079475?utm_source=tuicool

 

使用hystrix保护你的应用 - Kris的博客 | Kris' Blog

$
0
0

凡是可能出错的事必定会出错

豪猪

hystrix([hɪst'rɪks])是豪猪的意思。豪猪是一种哺乳动物,全身是刺用以更好的保护自己。netflix使用这畜生来命名这框架实在是非常的贴切,意味着hystrix能够像豪猪的刺一样保护着你的应用。下面是一张豪猪的高清无码大图

豪猪

本文专门探讨netflix的hystrix框架。首先会说明在一次请求中调用多个远程服务时可能会出现的雪崩问题,然后提出几个解决这些问题的办法,从而引出了hystrix框架;之后我们会给出一个简单的例子试图说明hystrix是如何解决上述问题的;文章主要探讨了线程池隔离技术、信号量隔离技术、优雅降级、熔断器机制。

从雪崩看应用防护

一个现实中常见的场景

我们先来看一个分布式系统中常见的简化的模型。此图来自hystrix的官方wiki,因为模型比较简单我这里就在不在重复画图,直接使用现成的图片做补充说明。

一个简单的模型

App Container可以是我们的应用容器,比如jettytomcat,也可以是一个用来处理外部请求的线程池(比如netty的worker线程池)。一个用户请求有可能依赖其他多个外部服务,比如上图中的A,H,I,P,在不可靠的网络环境下,任何的RPC都可能会面临三种情况:成功、失败、超时。如果一次用户请求所依赖外部服务(A,H,I,P)有任何一个不可用,就有可能导致整个用户请求被阻塞。考虑到应用容器的线程数目基本都是固定的(比如tomcat的线程池默认200),当在高并发的情况下,某一外部依赖的服务超时阻塞,就有可能使得整个主线程池被占满,这是长请求拥塞反模式

soa2

更进一步,线程池被占满就会导致整个服务不可用,而依赖该服务的其他服务,就又可能会重复产生上述问题。因此整个系统就像雪崩一样逐渐的扩散、坍塌、崩溃了!

产生原因

服务提供者不可用,从而导致服务调用者线程资源耗尽是产生雪崩的原因之一。除此之外还有其他因素能够产生雪崩效应:

  • 服务调用者自身流量激增,导致系统负载升高。比如异常流量、用户重试、代码逻辑重复
  • 缓存到期刷新,使得请求都流向数据库
  • 重试机制,比如我们rpc框架的retry次数,每次重试都可能会进一步恶化服务提供者
  • 硬件故障,比如机房断电,电缆被挖了….

常见的解决方案

针对上述雪崩情景,有很多应对方案,但没有一个万能的模式能够应对所有情况。

  1. 针对服务调用者自身流量激增,我们可以采用auto-scaling方式进行自动扩容以应对突发流量,或在负载均衡器上安装限流模块。参考微博:春节日活跃用户超一亿,探秘如何实现服务器分钟级扩容
  2. 针对缓存到期刷新,我们也有很多方案,参考Cache应用中的服务过载案例研究
  3. 针对重试机制,我们可以减少或关闭重试,直接采用failfast,或采用failsafe进行优雅降级。
  4. 针对硬件故障,我们可以做多机房容灾异地多活等。
  5. 针对服务提供者不可用,我们可以使用资源隔离熔断器机制等。参考Martin Fowler的熔断器模式

hystrix能够解决服务提供者不可用的场景。他采用了资源隔离模式,通过线程隔离和信号量隔离保护主线程池;使用熔断器避免无节操的重试,并提供断路自动复位功能。下面我们就来看一看如何使用hystrix。

使用hystrix

hystrix采用了命令模式,客户端需要继承抽象类HystrixCommand并实现其特定方法。为什么使用命令模式呢?使用过RPC框架都应该知道一个远程接口所定义的方法可能不止一个,为了更加细粒度的保护单个方法调用,命令模式就非常适合这种场景。命令模式的本质就是分离方法调用和方法实现,在这里我们通过将接口方法抽象成HystricCommand的子类,从而获得安全防护能力,并使得的控制力度下沉到方法级别。

命令模式

从简单例子入手

先来看一个简单的例子,TagService是一个远程接口,queryTags()是其中一个方法,我们将其封装为一个命令:

publicclassTagQueryCommandextendsHystrixCommand<List<String>>{
// queryTags()的入参
intgroupId;
// dubbo的实现接口
TagService remoteServiceRef;
// 构造方法用来进行方法参数传递
protectedTagQueryCommand(intgroupId){
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("TagService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("TagQueryCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("TagServicePool"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(THREAD)
.withCircuitBreakerEnabled(true)
));
this.groupId = groupId;
this.remoteServiceRef = ApplicationContextHelper.getBean(TagService.class);
}
// 我们调用远程方法定义在这里面
@Override
protectedList<String>run()throwsException{
returnremoteServiceRef.queryTags(groupId);
}
// 降级方式
@Override
protectedList<String>getFallback(){
returnCollections.emptyList();
}
}

在以往的编程实战中,我们大多是直接通过依赖注入的方式,注入rpc接口代理。但经过命令模式包装之后(使用HystrixCommand封装了TagService.queryTags()方法),我们每次的调用都需要动态的创建一个命令:

// 带有隔离机制和熔断器的远程调用
List<String> tags =newTagQueryCommand(1).execute()

以上的调用是阻塞的,他也等同于下面的代码:

Future<List<String>> f =newTagQueryCommand(1).queue();
List<String> tags = f.get();

我们也可以直接使用Future模式接口执行异步调用:

Future<List<String>> f =newTagQueryCommand(1).queue();
// 做一些额外工作
if(f.isDone()) {
f.get();
}

对于上述实例我们还有以下几个问题需要弄明白:

  1. 每次new命令对象开销怎么样?
  2. 构造方法中的那几个key分别是什么意思?
  3. 这里的隔离策略配置是什么意思?
  4. 如何去做优雅降级?
  5. 怎么开启和配置熔断器?

创建命令开销

每次new一个命令确实会有开销。但是如果查看HystrixCommand的源码,你会发现这个类的内部成员变量大都是共享对象。由于使用共享对象,每次创建一个新的command对象也就仅仅消耗一些引用空间以及一些非共享的原子状态变量。因此这个类仍然是比较轻量的,我们在继承这个类时,也应该继续保持轻量。由于做了一层封装,对cpu的额外消耗不可避免,但经过netflix的测试发现,带来的额外性能消耗与他能带来的好处相比是可以忽略不计

key的意义

接着,我们再来说一下构造方法中key的意义:

  1. HystrixCommandKey他用于唯一区分一个命令对象,并且唯一标识熔断器、metric等资源。我们可以为每一个远程方法都建立一个独一无二的key。如果key相同,意味着此时会共用熔断器和metric资源。
  2. HystrixCommandGroupKey将command进行分组,主要用于统计以便于我们进行监控。
  3. HystrixThreadPoolKey用来标示线程池,每一个command默认配备一个线程池(线程隔离模式下)。如果key相同,则会共用一个线程池资源。

一般实践中,我们将一个接口中的所有方法都用不同的命令key区分,组key采用类名,线程池则根据需要选择性的采用共享线程池或独立线程池。

正确选择隔离模式

hystrix之所以能够防止雪崩的本质原因,是其运用了资源隔离模式。要解释资源隔离的概念,我们可以用船舱做比喻。一艘游轮一般都是一个一个舱室隔离开来的,这样如果某一个舱室出现火灾,就不会波及到其他船舱,从而影响整艘游轮(这个是弹性工程学的一个关键概念:舱壁)。软件资源隔离如出一辙,上文已经说过,由于服务提供者不可用,可能导致服务调用端主线程池被占满。此时如果采用资源隔离模式,将对远程服务的调用隔离到一个单独的线程池后,若服务提供者不可用,那么受到影响的只会是这个独立的线程池。如图:

soa3

hystrix的线程池抽象是HystrixThreadPool类,它封装了JDK的ThreadPoolExecutor,然后通过并发策略HystrixConcurrencyStrategy对外提供工厂方法。我们这里只关心该线程池的核心配置,如下表:

参数解释
coreSize核心线程数,maxSize也是该值
keepAliveTime空闲线程保活时间
maxQueueSize最大队列大小,如果-1则会使用交换队列
queueSizeRejectionThreashold当等待队列多大的时候,将会执行决绝策略
timeoutInMilliseconds执行线程的超时时间

这里我们需要注意的是queueSizeRejectionThreashold配置,直接用maxQueueSize去限制队列大小行不行?行,但是不好,因为maxQueueSize是在初始化BlockingQueue时写死的,灵活性较差,queueSizeRejectionThreashold则能够动态进行配置,灵活性好,我们在调节线程池配置的时候也会重点关注这个值,如果设置的过高,则起不到隔离的目的(试想把他和maxQueueSize设置的非常大,则基本不会触发拒绝策略),如果设置过小,就难以应对突发流量,因为你的缓存队列小了,当并发突然上来后很快就会触发拒绝策略。因此需要根据实际的业务情况求得一个最佳值,当然也可以去做弹性感知。

除了线程池隔离,hystrix还提供了信号量隔离机制。所谓信号量隔离(TryableSemaphore),说的比较玄乎,其实很简单,就是采用资源计数法,每一个线程来了就去资源池判断一下是否有可用资源,有就继续执行,然后资源池信号量自减,使用完再自增回来;没有则调用降级策略或抛出异常。通过这种方式能够限制资源的最大并发数,但它有两个不好的地方:其一是他无法使用异步调用,因为使用信号量,意味着在调用者线程中执行run()方法;其二信号量不像线程池自带缓冲队列,无法应对突发情况,当达设定的并发后,就会执行失败。因此信号量更适用于非网络请求的场景中。信号量隔离模式下的最主要配置就是semaphoreMaxConcurrentRequests,用来设定最大并发量。我们再来看一下信号量的实现类,TrableSemaphore

privatestaticclassTryableSemaphore{
// 总资源数
privatefinalHystrixProperty<Integer> numberOfPermits;
// 当前资源数
privatefinalAtomicInteger count =newAtomicInteger(0);
publicTryableSemaphore(HystrixProperty<Integer> numberOfPermits){
this.numberOfPermits = numberOfPermits;
}
publicbooleantryAcquire(){
intcurrentCount = count.incrementAndGet();
if(currentCount > numberOfPermits.get()) {
count.decrementAndGet();
returnfalse;
}else{
returntrue;
}
}
publicvoidrelease(){
count.decrementAndGet();
}
publicintgetNumberOfPermitsUsed(){
returncount.get();
}
}

使用优雅降级

所谓的优雅降级本质上就是指当服务提供者不可用时,我们能够通过某种手段容忍这种不可用,以不影响正常请求。我们这里举个查询标签服务的例子,如果该服务不可用,是可以返回一组默认标签以提供优雅降级。比如,我们要查看大品类,它包括:家电、图书、音响等,这时我们可以在系统初始化中默认装载这一批兜底数据,当服务不可用,我们则降级到这些兜底数据上,虽然数据可能不完备,但基本可用。使用hystrix可以非常方便的添加优雅降级策略,只需要OverridegetFallback()方法就可以了。

// 降级方式
@Override
protectedList<String>getFallback(){
// 这里我们可以返回兜底数据
returnCollections.emptyList();
}

父类的getFallback()是直接抛出异常的,因此要想开启优雅降级,必须重写这个方法,并且需要确保配置withFallbackEnabled被开启。有的时候我们可能会在降级代码中访问远程数据(比如访问redis),那么当并发量上来之后,也需要保护我们的降级调用,此时可以配置withFallbackIsolationSemaphoreMaxConcurrentRequests参数,当调用降级代码的并发数超过阈值时会抛出REJECTED_SEMAPHORE_FALLBACK异常

降级有很多种玩法,官方wiki也说了几种降级策略,我们可以根据实际情况选择合适的降级策略:

  • failfast:表示马上抛出异常,即不会降级,比较适用于关键服务。
  • fail silent:或者叫做failsafe,默默的什么都不做,并发度最大
  • failback static:比如返回0,true,false等
  • failback stubbed:返回默认的数据,比如上文的默认标签
  • failback cache via network:通过网络访问缓存数据

使用熔断器

熔断器与家里面的保险丝有些类似,当电流过大,保险丝熔断以保护我们的电器。在没有熔断器机制保护下,我们可能会无节操的重试,这会持续加大服务端压力,造成恶性循环;如果直接关闭重试功能,当服务端又可用的时候,我们又该如何恢复?熔断器正好适合这种场景:当请求失败比率(失败/总数)达到一定阈值后,熔断器开启,并休眠一段时间,这段休眠期过后熔断器将处与半开状态(half-open),在此状态下将试探性的放过一部分流量(hystrix只支持single request),如果这部分流量调用成功后,再次将熔断器闭合,否则熔断器继续保持开启并进入下一轮休眠周期。

熔断器状态变迁

我们知道了熔断器的原理后,再重点看一下hystrix都支持哪些熔断器配置:

参数解释
enabled熔断器是否开启,默认开启
errorThresholdPercentage熔断器错误比率阈值
forceClosed是否强制闭合
forceOpen是否强制打开
requestVolumeThreshold表示请求数至少达到多大才进行熔断计算
sleepWindowInMilliseconds半开的触发试探休眠时间

errorThresholdPercentage用来设定错误比率,默认50%,比如在一段时间内我们有100个调用请求,其中有70个超时了,那么这段时间的错误比率是70%,它大于50%则会触发熔断器熔断。这个值的设定非常重要,他表示我们对错误的容忍程度,值越小我们对错误的容忍程度越小。强制闭合和强制打开是两个运行时调节动态参数,如果强制闭合则忽略统计信息,熔断器马上闭合,相反强制打开则会保证熔断器始终处于open状态。requestVolumeThreshold也是一个比较重要的参数,默认是20,表示至少有20个请求才进行熔断错误比率计算。什么意思?比如我有19个请求,但是全部失败了,错误比率100%,但也不会触发熔断,因为我的volume设定的是20。sleepWindowInMilliseconds是半开试探休眠时间,默认是5000ms,什么是试探休眠时间?上面我们说到了熔断器自动恢复的原理:当熔断器开启一段时间之后,再放过一部分流量进行试探。这一段时间就是试探休眠时间。如果这个值比较大,意味着我们可能需要一段比较长的恢复时间。如果值比较小,则表示我们需要更好地应对网络抖动情况。

hystrix抽象出HystrixCircuitBreaker接口用来提供熔断器功能,其在内部维护了AtomicBoolean circuitOpen作为熔断器状态开关。下面我们来看一下其实现的核心代码:

// 相关配置,就是我们上文在构造方法中的命令配置
privatefinalHystrixCommandProperties properties;
// 统计信息,按照时间窗口进行统计
privatefinalHystrixCommandMetrics metrics;
// 熔断器状态
privateAtomicBoolean circuitOpen =newAtomicBoolean(false);
// 熔断器打开时间或者上一次半开测试的时间,主要用于从休眠期恢复
privateAtomicLong circuitOpenedOrLastTestedTime =newAtomicLong();
// 外部调用者主要通过该方法获取熔断器状态
publicbooleanisOpen(){
if(circuitOpen.get()) {
// 如果熔断器是打开的,则返回true
returntrue;
}
// metric能够统计服务调用情况
HealthCounts health = metrics.getHealthCounts();
// 如果没有达到熔断器设定的volumn值则false,肯定是关闭的
if(health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
returnfalse;
}
// 如果错误比率也没有达到设定值,也会关闭的
if(health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
returnfalse;
}else{
// 熔断器开启
if(circuitOpen.compareAndSet(false,true)) {
//设定熔断器打开时间,主要为了进行休眠期判断
circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
returntrue;
}else{
returnfalse;
}
}
}
//做single request测试
publicbooleanallowSingleTest(){
longtimeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();
// 判断是否已经过了熔断器打开休眠期
if(circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + properties.circuitBreakerSleepWindowInMilliseconds().get()) {
// 这里将上一次测试时间设置为当前时间,主要为了休眠期判断
if(circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {
returntrue;
}
}
returnfalse;
}

后记

第一次听说熔断器模式还是在公司的tech邮件讨论组里,同事都在讨论一个故障:由于代码bug,导致请求时间变长,调用方又不断重试,结果使整组服务崩溃。这件事过后没多久,公司的RPC框架中就增加了熔断器机制。最近也在做motan的开源代码,想在其中增加一个熔断器的实现,于是翻了翻hystrix源代码,从中学习到了不少好东西:线程池隔离、信号量隔离、熔断器的实现、RxJava等等。当然hystrix的功能还不仅限于此,由于篇幅原因,还有很多内容并没有涉及到,比如请求缓存与上下文、collapse请求合并、metrics的实现、hystrix扩展钩子。

参考资料


AI大行其道,你准备好了吗?—谨送给徘徊于转行AI的程序员 - JeemyJohn的博客 - CSDN博客

$
0
0

前言

  近年来,随着 Google 的 AlphaGo 打败韩国围棋棋手李世乭之后,机器学习尤其是深度学习的热潮席卷了整个IT界。所有的互联网公司,尤其是 Google 微软,百度,腾讯等巨头,无不在布局人工智能技术和市场。百度,腾讯,阿里巴巴,京东,等互联网巨头甚至都在美国硅谷大肆高薪挖掘人工智能人才。现在在北京,只要是机器学习算法岗位,少则月薪 20k,甚至100k 以上……

  不错,新时代时代来了,我们从互联网走向移动互联网,现在又从移动互联网走向人工智能时代。业内有人称这一次的人工智能爆发是互联网3.0时代的开启。所以现在搞IT开发的工程师的不懂机器学习,就相当于低级程序员。赶紧从基础学起,入门机器学习,走进人工智能的大门……



这里写图片描述


1、人工智能的三起三落

  20世纪50-70年代,人工智能提出后,力图模拟人类智慧,但是由于过分简单的算法、匮乏的难以应对不确定环境的理论,以及计算能力的限制,逐渐冷却。

  

  20世纪80年代,人工智能的关键应用——专家系统得以发展,但是数据较少,难以捕捉专家的隐性知识,建造和维护大型系统的复杂性和成本也使得人工智能渐渐不被主流计算机科学所重视。

  

  进入20世纪90年代,神经网络、遗传算法等科技“进化”出许多解决问题的最佳方案,于是21世纪前10年,复兴人工智能研究进程的各种要素,例如摩尔定律、大数据、云计算和新算法等,推动人工智能在20世界20年代进入快速增长时期。预计未来十年,会在一些难以逾越的困惑中迎来奇点时代的爆发式增长。

2、新浪潮为什么会崛起

  人工智能(AI)问世之初曾经狂妄自大、令人失望,它如何突然变成当今最热门的技术领域?这个词语首次出现在1956年的一份研究计划书中。该计划书写道:“只要精心挑选一群科学家,让他们一起研究一个夏天,就可以取得重大进展,使机器能够解决目前只有人类才能解决的那些问题。”至少可以说,这种看法过于乐观。尽管偶有进步,但AI在人们心目中成为了言过其实的代名词,以至于研究人员基本上避免使用这个词语,宁愿用“专家系统”或者“神经网络”代替。“AI”的平反和当前的热潮可追溯到2012年的ImageNet Challenge在线竞赛。

  ImageNet是一个在线数据库,包含数百万张图片,全部由人工标记。每年一度的ImageNet Challenge竞赛旨在鼓励该领域的研究人员比拼和衡量他们在计算机自动识别和标记图像方面的进展。他们的系统首先使用一组被正确标记的图像进行训练,然后接受挑战,标记之前从未见过的测试图像。在随后的研讨会上,获胜者分享和讨论他们的技术。2010年,获胜的那个系统标记图像的准确率为72%(人类平均为95%)。2012年,多伦多大学教授杰夫·辛顿(Geoff Hinton)领导的一支团队凭借一项名为“深度学习”的新技术大幅提高了准确率,达到85%。后来在2015年的ImageNet Challenge竞赛中,这项技术使准确率进一步提升至96%,首次超越人类。



这里写图片描述


  不错,这一切都归功于一个概念:“深度学习(Deep Learning)”。虽然2016年之前,深度学习技术已经火了起来,但是真正大爆发的事件却是2016年Google在韩国首尔举行的人工智能机器人AlphaGo与围棋九段选手李世石之间的人机五翻棋大战,最终人类最强选手输给了机器人。曾几时何,人们认为围棋是人类棋牌类游戏的最后的尊严阵地,就这样在人工智能轻松地攻陷了人类智力的最后一块阵地!这件事震惊了所有人。从这以后,全球学术界和工业界都躁动了,巨头们都在加紧布局人工智能:Google挖来了神经网络算法的奠基人、深度学习之父 Geoffrey Hinton;Facebook则挖到了Hinton的学生,卷积神经网络(CNN)的奠基人Yann LeCun;然而就在不到一年的时间,微软也是说动了一直保持中立留在学术界的深度学习领域三大牛的最后一位Yoshua Bengio。当然,国内的互联网巨头,百度、阿里、腾讯、京东、滴滴、美团等也都在布局AI。其中百度更是被认为在AI上已经All In了。



这里写图片描述



深度神经网络(DNN)


3、机器学习是你必经之路

  入门AI,机器学习是必须要学习的,可以这么说:机器学习是人工智能的基石和精髓。只有学好了机器学习算法原理和思想,你才算真正的入门人工智能。但是,对于非专业的半路出家的你们该如何入门?这个问题其实很难回答,因为每个人的目标不一样,技术基础和数学基础也都不一样,所以因人而异。但是通常来说,学习机器学习算法,需要的必备知识还是可以罗列的。

3.1 机器学习必备基础

这里写图片描述
机器学习的学习过程


  对于上图,之所以最左边写了『数学基础』『经典算法学习』『编程技术』三个并行的部分,是因为机器学习是一个将数学、算法理论和工程实践紧密结合的领域,需要扎实的理论基础帮助引导数据分析与模型调优,同时也需要精湛的工程开发能力去高效化地训练和部署模型和服务。

  在互联网领域从事机器学习的人基本上属于以下两种背景:其中绝大部分是程序员出身,这类童鞋工程经验相对会多一些;另一部分是学数学统计领域的,这部分童鞋理论基础相对扎实一些。因此对比上图,这二类童鞋入门机器学习,所欠缺和需要加强的部分是不一样的。

3.1.1 关于数学

   曾经有无数的满怀激情,誓要在机器学习领域有一番作为的同学,在看到公式的一刻突然就觉得自己狗带了。是的,机器学习之所以门槛高并且显得高大上的主要原因就是数学。每一个算法,要在训练集上最大程度拟合同时又保证泛化能力,需要不断分析结果和数据,调优参数,这需要我们对数据分布和模型底层的数学原理有一定的理解。所幸的是如果只是想合理应用机器学习,而不是做相关方向高精尖的研究,所需要的数学知识读完本科的理工科童鞋还是能很容易的把这些数学知识学明白的。

   基本所有常见机器学习算法需要的数学基础,都集中在微积分、线性代数和概率与统计当中。下面我们先过一过知识重点,文章的后部分会介绍一些帮助学习和巩固这些知识的资料。

  • 微积分

       微分的计算及其几何、物理含义,是机器学习中大多数算法的求解过程的核心。比如算法中运用到梯度下降法、牛顿法等。如果对其几何意义有充分的理解,就能理解“梯度下降是用平面来逼近局部,牛顿法是用曲面逼近局部”,能够更好地理解运用这样的方法。

      凸优化和条件最优化的相关知识在算法中的应用随处可见,如果能有系统的学习将使得你对算法的认识达到一个新高度。

这里写图片描述
梯度下降法示意图

  • 线性代数

       大多数机器学习的算法要应用起来,依赖于高效的计算,这种场景下,程序员童鞋们习惯的多层for循环通常就行不通了,而大多数的循环操作可转化成矩阵之间的乘法运算,这就和线性代数有莫大的关系了。向量的内积运算更是随处可见。矩阵乘法与分解在机器学习的主成分分析(PCA)和奇异值分解(SVD) 等部分呈现刷屏状地出现。

      

    enter image description here


    奇异值分解过程示意图


       在机器学习领域,有相当多的应用与奇异值分解都有非常紧密的联系,比如机器学习中常做feature reduction的PCA,做数据压缩(以图像压缩为代表)的算法,还有做搜索引擎语义层次检索的LSI(Latent Semantic Indexing)

  • 概率与统计

       从广义来说,机器学习在做的很多事情,和统计层面数据分析和发掘隐藏的模式,是非常类似的。以至于传统的机器学习很大一部分被称作统计学习理论,这充分说明了统计学在机器学习领域的重要性。

      极大似然思想、贝叶斯模型是理论基础,朴素贝叶斯(NaiveBayes)、语言模型(Ngram)、隐马尔科夫(HMM)、隐变量混合概率模型是他们的高级形态。常见分布如高斯分布是混合高斯模型(GMM)等的基础。

enter image description here


朴素贝叶斯算法的基本原理

3.1.2 经典算法的学习

  机器学习中有很多的经典算法:感知机KNN朴素贝叶斯K-MeansSVMAdaBoostEM决策树随机森林GDBTHMM……

  算法这么多,那么对于初学者应该怎么学习呢?我的答案是:分门别类很重要。基本上,对机器学习算法的分类普遍的观点是分为三大类:有监督学习无监督学习强化学习

  • 有监督学习

      有监督学习是指进行训练的数据包含两部分信息:特征向量 + 类别标签。也就是说,他们在训练的时候每一个数据向量所属的类别是事先知道的。在设计学习算法的时候,学习调整参数的过程会根据类标进行调整,类似于学习的过程中被监督了一样,而不是漫无目标地去学习,故此得名。下图中,不同颜色的点代表不同的类别,直线就是我们学习出来的分界面(也叫学习器分类器)。

这里写图片描述


典型的有监督学习

  • 无监督学习

      相对于有监督而言,无监督方法的训练数据没有类标,只有特征向量。甚至很多时候我们都不知道总共的类别有多少个。因此,无监督学习就不叫做分类,而往往叫做聚类。就是采用一定的算法,把特征性质相近的样本聚在一起成为一类。

      K-Means算法就是一个无监督学习算法,在它执行前数据是每有类标的,执行过程中才会有类标,但是此时类标不固定,只有当聚类完成后每个样本的类标才能固定。如下图所示就是无监督算法的执行过程。

这里写图片描述
K-Means算法聚类过程

  • 强化学习

      所谓强化学习就是智能系统从环境到行为映射的学习,以使奖励信号(强化信号)函数值最大,强化学习不同于连接主义学习中的监督学习,主要表现在教师信号上,强化学习中由环境提供的强化信号是对产生动作的好坏作一种评价(通常为标量信号),而不是告诉强化学习系统RLS(reinforcement learning system)如何去产生正确的动作。由于外部环境提供的信息很少,RLS必须靠自身的经历进行学习。通过这种方式,RLS在行动-评价的环境中获得知识,改进行动方案以适应环境。

      本文是一个教你入门的方法,只是带你领略了解机器学习的大致框架。不会介绍具体的算法的详细原理与过程,所以想要深入了解可以查看相关书籍或者文献。对于具体的算法属于哪一类,并且为什么这么划分,请读者认真学习相关的机器学习教程。

3.1.3 编程技术

  对于编程技术学习和选择,无非就是编程语言开发环境。我个人的建议是Python+PyCharm。原因很简单,python简单易学,不至于让我们把太多的时间花在语言的学习上(PS:学习机器学习的重点在于各个机器学习算法理论的学习和掌握)。并且Jetbrains公司开发的Python集成开发环境PyCharm也是非常的简单易用。

这里写图片描述


Python与PyCharm

4、你是否真的准备好了?

  说完了机器学习的入门过程,我得给大家泼点冷水。虽然说目前AI真的很火热,就在刚刚,我写累了休息看新闻的时候就有新闻推送给我:商汤科技B轮融资4.5亿美元。这场革命是机遇,但是它真的适合你吗?我可以很肯定的说,并不是所有人都适合转行AI。

  

下面是的总结,想转行的人可以自我对照:

  1. 如果你天生感觉学习数学很吃力,并且代码能力很一般的人。我可以很负责人的告诉你,转行AI,学习机器学习算法将会是你人生的灾难。对于这类猿友你一定不能转行AI;

  2. 如果你数学一般,但是编程能力非常好,你曾经有着用代码改变世界的雄心。对于这一类猿友,我觉得你转行也行,但是你一定要走应用化的AI道路。因为数学是你的天花板,你注定成不了Hinton那样的学术大牛;

  3. 如果你数学很好,但是编程薄弱。恭喜你,你具备了转行AI的先天优势。对于这类猿友,我觉得你可以转行AI,但是你得努力把编程水平提上来。

  4. 如果你数学很牛,曾经与菲尔兹奖擦肩而过,曾经给Apache顶级项目贡献N万行核心代码。恭喜你,AI领域需要的就是你,你就是未来的Hinton吴恩达……

- - -AI大行其道,你准备好了吗?





对机器学习,人工智能感兴趣的小伙伴,请关注我的公众号:

这里写图片描述


参考文献:

  1. 李航. 统计学习方法[M]. 清华大学出版社, 2012.
  2. 周志华. 机器学习 : = Machine learning[M]. 清华大学出版社, 2016.
  3. 机器学习(三)常见算法优缺点

异地多活架构设计 - 博客频道 - CSDN.NET

$
0
0

https://yq.aliyun.com/articles/57715



1. 引言

有幸参与了阿里游戏的一个高可用方案的设计,并且在网上发表了方案(面向业务的立体化高可用架构设计),后来参加GOPS全球运维大会深圳站,与众多行业高手交流,发现大家对“异地多活”这个方案设计非常感兴趣,毕竟“异地多活”的方案价值非常大,尤其是互联网行业,规模稍微大一点几乎都必须是标配;但同时大家都觉得“异地多活”的方案设计又很难,网络、数据、事务等各种问题混杂在一起,很多问题看似是无法解决的。比如说:“网络断了怎么保证数据一致性”、“怎么保证异地事务一致性”、“业务怎么无缝的在多个地点切换”。。。。。。等等。

 其实大部分问题我们之前也遇到过,这些问题当时也困扰着我们,后来我们经过讨论和思考,发现其实很多时候我们困扰的主要原因是过于“追求完美的异地多活方案”,这样导致“异地多活”设计中出现很多了的思维误区,而如果不意识到这些思维误区,就会陷入死胡同,导致无法实现真正的“异地多活”方案。

 接下来我将总结常见的思维误区,看看你踩中了哪个坑? 

2. 所有业务异地多活

“异地多活”是为了保证业务的高可用,但很多朋友在考虑这个“业务”的时候,会不自觉的陷入一个思维误区:我要保证所有业务的“异地多活”

 比如说假设我们需要做一个“用户子系统”,这个子系统负责“注册”、“登录”、“用户信息”三个业务。为了支持海量用户,我们设计了一个“用户分区”的架构,即:正常情况下用户属于某个主分区,每个分区都有其它数据的备份,用户用邮箱或者手机号注册,路由层拿到邮箱或者手机号后,通过hash计算属于哪个中心,然后请求对应的业务中心。基本的架构如下: 



9dd2bbb38e092fe164d082aa2bca252646281290

 

考虑这样一个系统,如果3个业务要同时实现异地多活,我们会发现如下一些难以解决的问题:

【注册】

A中心注册了用户,数据还未同步到B中心,此时A中心宕机,为了支持注册业务多活,那我们可以挑选B中心让用户去重新注册。看起来很容易就支持多活了,但仔细思考一下会发现这样做会有问题:一个手机号只能注册一个账号,A中心的数据没有同步过来,B中心无法判断这个手机号是否重复,如果B中心让用户注册,后来A中心恢复了,发现数据有冲突,怎么解决?实际上是无法解决的,因为注册账号不能说挑选最后一个生效;而如果B中心不支持本来属于A中心的业务进行注册,注册业务的双活又成了空谈。

 有的朋友可能会说:那我修改业务规则,允许一个手机号注册多个账号不就可以了么?

这样做是不可行的,类似一个手机号只能注册一个账号这种规则,是核心业务规则,修改核心业务规则的代价非常大,几乎所有的业务都要重新设计,为了架构设计去改变业务规则,而且是这么核心的业务规则是得不偿失的。

 【用户信息】

用户信息的修改和注册有类似的问题,即:A、B两个中心在异常的情况下都修改了用户信息,如何处理冲突?

由于用户信息并没有账号那么关键,一种简单的处理方式是按照时间合并,即:最后修改的生效。业务逻辑上没问题,但实际操作也有一个很关键的坑:怎么保证多个中心所有机器时间绝对一致?在异地多中心的网络下,这个是无法保证的,即使有时间同步也无法完全保证,只要两个中心的时间误差超过1s,数据就可能出现混乱,即:先修改的反而生效。

 还有一种方式是生成全局唯一递增ID,这个方案的成本很高,因为这个全局唯一递增ID的系统本身又要考虑异地多活,同样涉及数据一致性和冲突的问题。

 综合上面的简单分析,我们可以发现,如果“注册”“登录”、“用户信息”全部都要支持异地多活的话,实际上是挺难的,有的问题甚至是无解的。那这种情况下我们应该如何考虑“异地多活”的方案设计呢?答案其实很简单:优先实现核心业务的异地多活方案

 对于我们的这个模拟案例来说,“登录”才是最核心的业务,“注册”和“用户信息”虽然也是主要业务,但并不一定要实现异地多活。主要原因在于业务影响。对于一个日活1000万的业务来说,每天注册用户可能是几万,修改用户信息的可能还不到1万,但登录用户是1000万,很明显我们应该保证登录的异地多活。对于新用户来说,注册不了影响并不很明显,因为他还没有真正开始业务;用户信息修改也类似,用户暂时修改不了用户信息,对于其业务不会有很大影响,而如果有几百万用户登录不了,就相当于几百万用户无法使用业务,对业务的影响就非常大了:公司的客服热线很快就被打爆了,微博微信上到处都在传业务宕机,论坛里面到处是在骂娘的用户,那就是互联网大事件了!

 而登录实现“异地多活”恰恰是最简单的,因为每个中心都有所有用户的账号和密码信息,用户在哪个中心都可以登录。用户在A中心登录,A中心宕机后,用户到B中心重新登录即可。

 有的朋友可能会问,如果某个用户在A中心修改了密码,此时数据还没有同步到B中心,用户到B中心登录是无法登录的,这个怎么处理?这个问题其实就涉及另外一个思维误区了,我们稍后再谈。

 

3. 实时一致性

异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活设计方案的核心,但我们大部分人在考虑数据同步方案的时候,也会不知不觉的陷入完美主义误区:我要所有数据都实时同步

 数据冗余就要将数据从A地同步到B地,从业务的角度来看是越快越好,最好和本地机房一样的速度最好,但让人头疼的问题正在这里:异地多活理论上就不可能很快,因为这是物理定律决定的,即:光速真空传播是每秒30万公里,在光纤中传输的速度大约是每秒20万公里,再加上传输中的各种网络设备的处理,实际还远远达不到光速的速度。

 除了距离上的限制外,中间传输各种不可控的因素也非常多,例如挖掘机把光纤挖断,中美海底电缆被拖船扯断、骨干网故障等,这些故障是第三方维护,我们根本无能为力也无法预知。例如广州机房到北京机房,正常情况下RTT大约是50ms左右,遇到网络波动之类的情况,RTT可能飙升到500ms甚至1s,更不用说经常发生的线路丢包问题,那延迟可能就是几秒几十秒了。

 因此异地多活方案面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。

 既然是无法彻底解决的矛盾,那就只能想办法尽量减少影响。有几种方法可以参考:

  1. 尽量减少异地多活机房的距离,搭建高速网络;
  2. 尽量减少数据同步;
  3. 保证最终一致性,不保证实时一致性;

 

【减少距离:同城多中心】

为了减少两个业务中心的距离,选择在同一个城市不同的区搭建机房,机房间通过高速网络连通,例如在北京的海定区和通州区各搭建一个机房,两个机房间采用高速光纤网络连通,能够达到近似在一个机房的性能。

这个方案的优势在于对业务几乎没有影响,业务可以无缝的切换到同城多中心方案;缺点就是无法应对例如新奥尔良全城被水淹,或者2003美加大停电这种极端情况。所以即使采用这种方案,也还必须有一个其它城市的业务中心作为备份,最终的方案同样还是要考虑远距离的数据传输问题。 

【减少数据同步】

另外一种方式就是减少需要同步的数据。简单来说就是不重要的数据不要同步,同步后没用的数据不同步。

以前面的“用户子系统”为例,用户登录所产生的token或者session信息,数据量很大,但其实并不需要同步到其它业务中心,因为这些数据丢失后重新登录就可以了。



有的朋友会问:这些数据丢失后要求用户重新登录,影响用户体验的呀!

确实如此,毕竟需要用户重新输入账户和密码信息,或者至少要弹出登录界面让用户点击一次,但相比为了同步所有数据带来的代价,这个影响完全可以接受,其实这个问题也涉及了一个异地多活设计的典型思维误区,后面我们会详细讲到。 

【保证最终一致性】

第三种方式就是业务不依赖数据同步的实时性,只要数据最终能一致即可。例如:A机房注册了一个用户,业务上不要求能够在50ms内就同步到所有机房,正常情况下要求5分钟同步到所有机房即可,异常情况下甚至可以允许1小时或者1天后能够一致。

 最终一致性在具体实现的时候,还需要根据不同的数据特征,进行差异化的处理,以满足业务需要。例如对“账号”信息来说,如果在A机房新注册的用户5分钟内正好跑到B机房了,此时B机房还没有这个用户的信息,为了保证业务的正确,B机房就需要根据路由规则到A机房请求数据(这种处理方式其实就是后面讲的“二次读取”)。

而对“用户信息”来说,5分钟后同步也没有问题,也不需要采取其它措施来弥补,但还是会影响用户体验,即用户看到了旧的用户信息,这个问题怎么解决呢?这个问题实际上也涉及到了一个思维误区,在最后我们统一分析。 

4. 只使用存储系统的同步功能

数据同步是异地多活方案设计的核心,幸运的是基本上存储系统本身都会有同步的功能,例如MySQL的主备复制、Redis的Cluster功能、elasticsearch的集群功能。这些系统本身的同步功能已经比较强大,能够直接拿来就用,但这也无形中将我们引入了一个思维误区:只使用存储系统的同步功能

 既然说存储系统本身就有同步功能,而且同步功能还很强大,为何说只使用存储系统是一个思维误区呢?因为虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求。

 以MySQL为例,MySQL5.1版本的复制是单线程的复制,在网络抖动或者大量数据同步的时候,经常发生延迟较长的问题,短则延迟十几秒,长则可能达到十几分钟。而且即使我们通过监控的手段知道了MySQL同步时延较长,也难以采取什么措施,只能干等。

 Redis又是另外一个问题,Redis 3.0之前没有Cluster功能,只有主从复制功能,而为了设计上的简单,Redis主从复制有一个比较大的隐患:从机宕机或者和主机断开连接都需要重新连接主机,重新连接主机都会触发全量的主从复制,这时候主机会生成内存快照,主机依然可以对外提供服务,但是作为读的从机,就无法提供对外服务了,如果数据量大,恢复的时间会相当的长。

 综合上述的案例可以看出,存储系统本身自带的同步功能,在某些场景下是无法满足我们业务需要的。尤其是异地多机房这种部署,各种各样的异常都可能出现,当我们只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。

 解决的方案就是拓开思路,避免只使用存储系统的同步功能,可以将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案。

 例如,还是以前面的“用户子系统”为例,我们可以采用如下几种方式同步数据:

  1. 消息队列方式:对于账号数据,由于账号只会创建,不会修改和删除(假设我们不提供删除功能),我们可以将账号数据通过消息队列同步到其它业务中心。
  2. 二次读取方式:某些情况下可能出现消息队列同步也延迟了,用户在A中心注册,然后访问B中心的业务,此时B中心本地拿不到用户的账号数据。为了解决这个问题,B中心在读取本地数据失败的时候,可以根据路由规则,再去A中心访问一次(这就是所谓的二次读取,第一次读取本地,本地失败后第二次读取对端),这样就能够解决异常情况下同步延迟的问题。
  3. 存储系统同步方式:对于密码数据,由于用户改密码频率较低,而且用户不可能在1s内连续改多次密码,所以通过数据库的同步机制将数据复制到其它业务中心即可,用户信息数据和密码类似。
  4. 回源读取方式:对于登录的session数据,由于数据量很大,我们可以不同步数据;但当用户在A中心登录后,然后又在B中心登录,B中心拿到用户上传的session id后,根据路由判断session属于A中心,直接去A中心请求session数据即可,反之亦然,A中心也可以到B中心去拿取session数据。
  5. 重新生成数据方式:对于第4中场景,如果异常情况下,A中心宕机了,B中心请求session数据失败,此时就只能登录失败,让用户重新在B中心登录,生成新的session数据。

(注意:以上方案仅仅是示意,实际的设计方案要比这个复杂一些,还有很多细节要考虑) 

综合上述的各种措施,最后我们的“用户子系统”同步方式整体如下: 3d8b9d29d589937c40a266a0fc5b6dd5a0275ffb 

 

5. 100%可用性

前面我们在给出每个思维误区对应的解决方案的时候,其实都遗留了一些小尾巴:某些场景下我们无法保证100%的业务可用性,总是会有一定的损失。例如密码不同步导致无法登录、用户信息不同步导致用户看到旧的用户信息等等,这个问题怎么解决?

 其实这个问题涉及异地多活设计方案中一个典型的思维误区:我要保证业务100%可用!但极端情况下就是会丢一部分数据,就是会有一部分数据不能同步,怎么办呢,有没有什么巧妙和神通的办法能做到?

 很遗憾,答案是没有!异地多活也无法保证100%的业务可用,这是由物理规律决定的,光速和网络的传播速度、硬盘的读写速度、极端异常情况的不可控等,都是无法100%解决的。所以针对这个思维误区,我的答案是“忍”!也就是说我们要忍受这一小部分用户或者业务上的损失否则本来想为了保证最后的0.01%的用户的可用性,做个完美方案,结果却发现99.99%的用户都保证不了了。

 对于某些实时强一致性的业务,实际上受影响的用户会更多,甚至可能达到1/3的用户。以银行转账这个业务为例,假设小明在北京XX银行开了账号,如果小明要转账,一定要北京的银行业务中心是可用的,否则就不允许小明自己转账。如果不这样的话,假设在北京和上海两个业务中心实现了实时转账的异地多活,某些异常情况下就可能出现小明只有1万存款,他在北京转给了张三1万,然后又到上海转给了李四1万,两次转账都成功了。这种漏洞如果被人利用,后果不堪设想。

 当然,针对银行转账这个业务,可以有很多特殊的业务手段来实现异地多活。例如分为“实时转账”和“转账申请”。实时转账就是我们上述的案例,是无法做到“异地多活”的;但“转账申请”是可以做到“异地多活”的,即:小明在上海业务中心提交转账请求,但上海的业务中心并不立即转账,而是记录这个转账请求,然后后台异步发起真正的转账操作,如果此时北京业务中心不可用,转账请求就可以继续等待重试;假设等待2个小时后北京业务中心恢复了,此时上海业务中心去请求转账,发现余额不够,这个转账请求就失败了。小明再登录上来就会看到转账申请失败,原因是“余额不足”。不过需要注意的是“转账申请”的这种方式虽然有助于实现异地多活,但其实还是牺牲了用户体验的,对于小明来说,本来一次操作的事情,需要分为两次:一次提交转账申请,另外一次要确认是否转账成功。  

虽然我们无法做到100%可用性,但并不意味着我们什么都不能做,为了让用户心里更好受一些,我们可以采取一些措施进行安抚或者补偿,例如:

  1. 挂公告:说明现在有问题和基本的问题原因,如果不明确原因或者不方便说出原因,可以说“技术哥哥正在紧急处理”比较轻松和有趣的公告。
  2. 事后对用户进行补偿:例如送一些业务上可用的代金券、小礼包等,降低用户的抱怨。
  3. 补充体验:对于为了做异地多活而带来的体验损失,可以想一些方法减少或者规避。以“转账申请”为例,为了让用户不用确认转账申请是否成功,我们可以在转账成功或者失败后直接给用户发个短信,告诉他转账结果,这样用户就不用不时的登录系统来确认转账是否成功了。 

6. 一句话谈“异地多活”

综合前面的分析,异地多活设计的理念可以总结为一句话:采用多种手段,保证绝大部分用户的核心业务异地多活



Viewing all 532 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>