Git团队开发中PR工作模式的反思
Tag git, pr, 工作流, on by view 3225

在公司团队协作用了大半年的Git了,PR工作模式也用了半年。由一个用Git装逼的假老手成长为真正用Git进行团队协作开发的老油条。这其中也有一些值得反思的东西。我很崇拜PR的工作模式,因为它基本上是优秀的开源软件工作模式的代表,众多的开发者自发的给开源项目发送PR,以前总是盼望着自己的开源项目会有人发送PR,若是接收到PR就像是收到别人的礼物一般高兴。

我之前赞同的PR工作模式是这样的,开发者拿到开发需求之后,在自己的分支开发,然后向主仓库的相关分支发送PR,之后由测试人员在测试机上拉取主仓库相关分支的代码,然后fetch PR所在仓库分支的代码合并(merge)到本地主仓库相关分支,进行测试,测试完毕通过之后才合并PR到分支。我这一想法的灵感来源于 travis-ci 单元测试的工作模式:开发人员发送PR,travis-ci自动进行单元测试,PR管理者参考PR在 travis-ci 上单元测试的结果进行初步判定是否可以合并。但是我忽略了一点,测试人员并非是 travis-ci 上的单元测试。在工作的过程中往往是这样的:

开发人员初步开发出某个功能,然后经过自己的初步测试,发送PR到主仓库,测试人员看到有PR了,需要测试了,于是开始测试,但是测试有问题,于是开发人员就必须继续修改提交,然后再次测试,还有一些问题被发现依然存在,继续开发……可是,你突然发现,你的PR被合并了或者是被关闭了,因为要下班了;或者是要下班了,PR管理员会问这个PR能否合并,测试人员暂时没发现新的问题,于是测试人员告诉PR管理员“没有问题”,于是PR被合并了。或者没有上面的“PR管理员的询问”,但不管怎样,最后的结果往往是PR被草草的合并了。于是,没有充分测试的PR进入了仓库,甚至进入了发布分支,最终的结果可想而知。

在这个过程中,人们往往没有意识到其实是被PR给影响到了。因为开发者一个PR创立之后,在他会有一个潜意识希望PR被合并,测试人员同样也有这么一个潜意识,因为测试PR就是他的工作之一,他也希望PR被合并,对于PR管理员来说同样也有这么一个潜意识,希望PR被处理,不管是合并还是关闭。就是这些潜意识推动这个PR尽早被合并。

我个人感觉正确的做法应该是当测试人员测试充分之后再发起PR,避免这些潜意识促使PR过早的被合并。事实上,不管PR是否存在,测试人员都能够从开发者的分支上拉取到开发者的代码合并到主仓库,PR不过是一个形式而已,给合并者带来方便。但是那些错误的理解了PR的人们往往会搞出诸多莫名其妙的东西,比如PR用来人工做代码审核,比如搞个CheckList让人工去查代码中哪儿写得不规范,这些应该是让 Sonar 自动扫描PR中存在的不规范,谁又会有时间去为别人的PR人工做Sonar该做的事情。经过测试人员的充分测试之后,开发者创建PR,这时单元测试测试一遍,然后Sonar扫描一遍,后两者自动化测试通过之后代码才能入库。

总之,测试人员功能测试通过之后开发者才能创建PR,自动化工具进行单元测试和Sonar扫描通过后,PR管理员才能允许合并。这才是比较靠谱的PR工作流。


结构化数据缓存管理设计
Tag 数据库, 缓存, 管理, on by view 3347

现代web开发中,在数据库和应用程序服务器之间使用缓存是常见的手段,它有效的减轻了后端数据库服务器的压力。结构化数据库中的数据是按照表、字段这种结构存储的,而作为缓存系统的memcached或是redis-server是以key-value的方式存储数据的。如何将结构化的数据存储到非结构化的缓存系统是一个问题,对于MySQL等结构化数据库我们可以按照以下方式存储。

首先,对于表中单条数据我们可以按照Bean存储在缓存中,key为“表名+id”,value为Bean对象。

然后,对于列表查询,我们可以使用“select id from article where status=1 limit 10”这种语句查出article表的id列表 List<Long> 。然后通过id从缓存中逐条加载 Bean ,最终形成我们需要的 List<Bean>。而我们可以将通过sql查出来的List<Long>结构的id数组作为一个缓存对象,其key可以按照某种规则生成,例如“方法名+override_index+参数...”。

上面说的两种数据对象的存储方式都没有问题,而其中的难点在于如何管理缓存,我们知道,一个列表查询的结果List<Long>在这张表发生增删改的时候都会发生变化,因此,详细来说我们应该在列表的某个字段发生增删改的时候清理掉这个id列表缓存,等下次查询的时候再自动重建新的缓存,以保证缓存的正确性。不过,若是联表查询呢?联表查询“select user.id from article, user where article.userid=user.id and article.status=1”查出的是一个id列表,我们将其缓存假设key为key1,但是一旦article表和user表其中之一发生增删改,那么这个缓存数据应当失效,也就是说,Article在进行 Update()、Save()、Delete() 的时候我们应该清理掉这个key为key1的缓存,同样对于User在进行 Update()、Save()、Delete() 的时候我们也应当清理掉 key1 的缓存。在这儿只是列举了两个表联查,并且只是提到了一个联表查询,假如有N个表联表,而且有M个联表查询语句那么我们应该在多少个方法中调用清理这种缓存的方法呢M*N*3,好多好麻烦呀。

为了管理list的缓存,定义一个树形结构如下

cache_manage_struct.png

t开头的节点表示表名,tA、tB 表示 A 表和 B 表,f开头的表示字段,fAa 表示 A 表 a 字段,fAb 表示 A 表 b 字段,k开头的表示列表的缓存键。这个数据结构表达了我们应该如何清理缓存,比如A表中的a字段发生的改变(增删改),那么我应该清理掉键名为k1, k2, k3的缓存,A表字段b发生增删改时应该清理掉键名为k1, k2的缓存,B表a字段发生增删改时应该清理掉键名为 k1 的缓存。然后将该树形结构拆分为三级,分别用三类的key-value存储起来就好,也不必担心结构过于庞大,上图可以拆为6个key-value:

[0]    _init->(tA, tB)
[1]    tA->(fAa, fAb)
[1]    tB->(fBa, fBb)
[2]    fAa->(k1, k2, k3)
[2]    fAb->(k1, k2)
[2]    fBa->(k1)

上图的结构来自于如下三个SQL查询

[K1]   select id from A, B  where  A.fAa = ? and A.fAb = ? and B.fBa = ? ...
[K2]   select id from A     where  A.fAa = ? and A.fAb = ? ...
[K3]   select id from A     where  A.fAa = ? ...



状态码设计的智慧,1234还是1248
Tag 状态码, 设计, on by view 3492

一个事物有5个流程,ABCDE一般的程序员通常会定义为 status 有5个状态 0/1/2/3/4 ,流程的顺序是 A->B->C->D->E ,可是有一天项目经理说这个流程会有个快捷流程 A->B->E 。于是原本的 0->1->2->3->4 就有部分数据变为了 0->1->4 ,这些都不会有太大的问题,然而,后来项目经理跟你说我需要区分在E状态下它是经历了 ABCDE 还是直接经历 ABE ,于是乎,你就懵逼了。

有另外一种设计方式倒是可以获取详细的经历的流程结点,那就是 A:1, B:2, C:4, D:8, E:16 。于是A->B->C->D->E 表示的状态码切换过程为 1->3->7->15->31 。A->B->E 表示的状态码切换过程为 1->3->20, 有人说,你这样当处于E状态时就有可能有两个状态码了31和20,我如何判定他是处于E状态呢,其实这是位运算上的一个小技巧,我们可以很简单的判定出他是否在E状态 status & 0x10 > 0 若计算结果为true,则表示有经历E状态,按照你的流程方向便可以判定是处于哪一未置

switch (true) {
	case status & 0x10:
		return "E";
	case status & 0x08:
		return "D";
	case status & 0x04:
		return "C";
	case status & 0x02:
		return "B";
	case status & 0x01:
		return "A";
}

这一设计方式在ERP等系统的权限控制中更是常见

例如某一项目下有10个操作不同的用户可以配置不同的操作权限,那么就有 0b0000000000 到 0b1111111111 ,呃……,1024种权限配置方式,如果是100个【weightValue.size()】操作,那么就有 2^100 种权限配置方式。我们只需要给该用户一个 int(11) 类型的字段就可以完成权限配置。

又例如,某简历需要计算简历完整度,简历中分为10个【weightValue.size()】模块,不同的模块占分权值不同,那么我们也可以用一个字段来标记该简历的各模块的完成状况,0b0000000000 到 0b1111111111 ,你可以这样计算分值

int value = 0;
int weightValueNumber = weightValue.size();
int zerokeep = math.pow(2, weightValueNumber);
status = status + zerokeep;
for (int i = 0; statusMove - zerokeep > 0; i++) {
	statusMove = status < (i + 2); // 左移 (i+1)+1 位
	int nakedValue = (statusMove > (i + 2)) ^ status - zerokeep;
	boolean checked = nakedValue > 0;
	if (checked) {
		value += weightValue.get(i);
	}
	status = nakedValue + zerokeep;
}
return value;

这种算法的始祖应该算是 Linux 系统上的权限管理机制吧,chmod 777 /xxx 这种操作后面的智慧便是如此。若是铁定的不涉及到取某个结点ABCDE的独立状态详情,那么你可以随意定义状态码 1/2/3/4/5 或是 -2/-1/0/1/2 随你所好,但是假如某一天你遇上了坑爹的项目经理突然告诉你需要知道是否经历了CD状态,那么你在懵逼的时候是不是也会想起Linux系统上 chmod 指令的智慧呢?1234还是1248取决于你最初的设计。


Linux内核的安装
Tag linux, 内核, 安装, kernel, on by view 3169

此文不介绍Linux内核的编译及编译配置,只讲述在编译完成之后的内核部署相关内容,此文环境 CentOS 7,内核源码版本 4.4.12 。注意编译安装完成后 /usr/src/linux-4.4.12 目录不能删除

内核编译完成,编译所在目录 /usr/src ,接下来执行

make modules_install

他会将编译后的文件拷贝到 /lib/modules/4.4.12 目录,接下来拷贝内核

cp /usr/src/linux-4.4.12/arch/x86_64/boot/bzImage /boot/vmlinuz-4.4.12.x86_64

进入 /boot 目录并开始创建临时文件系统

cd /boot
mkinitrd initramfs-4.4.12.img 4.4.12

将会从 /lib/modules/4.4.12 目录生成 initramfs-4.4.12.x86_64.img 文件,便是临时文件系统,我们可以看到该目录下的文件目录

➜  4.4.12 ls -al
drwxr-xr-x.  3 root root   4096 6月   7 22:31 .
drwxr-xr-x.  4 root root     47 6月   7 21:44 ..
lrwxrwxrwx.  1 root root     21 6月   7 21:44 build -> /usr/src/linux-4.4.12
drwxr-xr-x. 13 root root   4096 6月   7 22:31 kernel
lrwxrwxrwx.  1 root root     21 6月   7 22:30 source -> /usr/src/linux-4.4.12

可以看到build目录链接到 /usr/src/linux-4.4.12 ,因此上面说不能删除该源码目录,重命名 img 文件

mv initramfs-4.4.12.img initramfs-4.4.12.x86_64.img

为何我们要将这些文件名命名成这样呢,我们可以看到 boot 目录下的文件如下

➜  /boot ls
config-3.10.0-327.el7.x86_64
grub
grub2
initramfs-0-rescue-a21a5e0aa76b4b0c85aeb76161573b23.img
initramfs-3.10.0-327.el7.x86_64.img
initramfs-3.10.0-327.el7.x86_64kdump.img
initramfs-4.4.12.x86_64.img
initrd-plymouth.img
symvers-3.10.0-327.el7.x86_64.gz
System.map-3.10.0-327.el7.x86_64
vmlinuz-0-rescue-a21a5e0aa76b4b0c85aeb76161573b23
vmlinuz-3.10.0-327.el7.x86_64
vmlinuz-4.4.12.x86_64

可以发现,centos 7 系统原来的内核命名已经临时文件系统命名是有规律的,vmlinuz-<version>.<arc> 是内核命名规则,initramfs-<version>.<arc>.img 是文件系统文件名。这么有规律的文件名是因为 centos 7 的 grub2 的默认配置文件中是按照这种规则自动搜索内核的,所以,centos 7 中如此命名新的内核可以不必额外配置 grub2 的配置文件,当然如果你熟悉 grub2 的配置,你也可以任意命名,在配置文件中写入正确的配置即可。

参考文献:

[1] http://blog.chinaunix.net/uid-24782829-id-3211008.html



grub2 配置修改
Tag grub2, 配置, on by view 2674

grub是linux系统的引导程序,grub2相对于grub有较大的改变,centos7使用的便是grub2,/boot/grub2/grub.cfg 便是grub2的引导配置,但是你修改配置文件确不应该修改该文件。/boot/grub2/grub.cfg 这个文件是用工具生成的,工具是 grub2-mkconfig 。他所对应的源文件存储在 /etc/grub.d 下面

➜  ~ ls /etc/grub.d 
00_header  01_users  20_linux_xen     30_os-prober  41_custom
00_tuned   10_linux  20_ppc_terminfo  40_custom     README

其中,40_custom 是你的配置应该添加的地方,比如我新编译了一个内核,希望添加一个新内核的启动项,我应该编辑 40_custom 入下

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.
menuentry "My custom boot entry" {
   set root=(hd0,1)
   linux /boot/vmlinuz-4.4.12.x86_64
   initrd /boot/initrd-4.4.12.img
}

然后执行 grub2-mkconfig --output=/boot/grub2/grub.cfg ,你会发现在新生成的 grub.cfg 文件中包含了你的引导项配置,然后重启看看就可以发现新的启动项了。

参考文献:

[1] http://superuser.com/questions/781300/searching-for-grub-configuration-file-in-centos-7


mysql数据库的初始化
Tag mysql, 初始化, on by view 3058

对于编译安装的mysql数据库,编译安装完成后首先就要进行简单的配置和初始化。

mysql 5.5版本

配置服务

cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql
chkconfig --add mysql
chkconfig mysql on

编辑配置文件 /etc/my.cnf,默认datadir=/var/lib/mysql,确认/var/lib/mysql目录存在,默认log-error=/var/log/mariadb/mariadb.log,确认/var/log/mariadb/mariadb.log文件存在,若不存在则创建。后续启动服务器可以监控日志文件查看错误日志。

如此配置后便可以通过service mysql start启动mysql服务了,不过此时无法成功启动,还需初始化mysql系统表

cd /usr/local/mysql
./scripts/mysql_install_db

使用 service mysql start 启动服务器便可以正常启动。

mysql 5.7版本

配置服务与上述5.5版本一致,按下面方法初始化数据库

cd /usr/local/mysql/bin
./mysql_install_db --basedir=/usr/local/mysql --datadir=/usr/local/mysql --user=root --force

使用 service mysql start 启动服务器。


mysql 5.7重置密码
Tag mysql, 密码, 重置, on by view 3518

在mysql 5.7中刚配置好的服务器会有一个随机生成的root密码,此时需要重置密码,或者用户忘记root密码时也需要重置密码,mysql 5.7的密码重置与之前版本有所不同。

修改my.cnf,在[mysqld]字段下增加skip-grant-tables字段,用于忽略权限验证,此时service mysql restart重启服务器,然后就可以无密码登录数据库了。并且在[mysqld]下添加default_password_lifetime=0设置密码不失效。

./mysql -uroot

登录到服务器后刷新权限

FLUSH PRIVILEGES;

然后执行下面的sql修改root密码

update mysql.user set authentication_string=password('new_password') where user='root' and Host ='localhost';

刷新权限并退出mysql客户端

flush privileges;
exit;

然后修改mysql.cnf,将之前添加的skip-grant-tables字段删除,保存退出。重启服务器

service mysql restart

再用mysql连接服务器试试看,此时需要密码,使用你的新密码便可以登录了。


网站升级至HTTP2
Tag http2, nginx, on by view 4339

HTTP2从提出到现在已经有一段时间了,不过目前使用该协议的网站并不多。不过著名如google, twitter, youtube他们都已经领先升级到了HTTP2协议。今天我也将自己的博客升级到了HTTP2。

重新编译安装最新的主线版本nginx 1.9.9

./configure --with-http_v2_module --with-http_ssl_module
make
make install

修改nginx配置文件

server {
    listen       443    ssl http2 fastopen=3 reuseport;
    server_name  duguying.net;

    ssl on;
    ssl_certificate /root/ssl/1_duguying.net_bundle.crt;
    ssl_certificate_key /root/ssl/2_duguying.net.key;

    location / {
        try_files /_not_exists_ @backend;
    }

    location @backend {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Host            $http_host;

        proxy_pass http://127.0.0.1:81;
    }
}

主要是添加 ssl http2 fastopen=3 reuseport

重启nginx服务。

接下来可以在浏览器上看到如下闪电图标(需要装插件 HTTP/2 and SPDY indicator)

http2.png


关于阿里云ESC上go语言项目编译6l: running gcc failed: Cannot allocate memory
Tag golang, 编译, 内存不够, swap, on by view 4305

前段时间将自己的阿里云服务器上的系统由centos 6.5换为了ubuntu 14,其他的硬件配置都没有发生改变,将服务器上的数据恢复并且重新安装了golang的编译环境后,发现使用go build编译稍微大一点的golang项目就会报错:

/usr/local/go/pkg/tool/linux_amd64/6l: running gcc failed: Cannot allocate memory

一直想不通为啥换了个系统就会报这个错,字面意思是gcc分配内存失败,应该是内存不够用,机器配置是1G内存,free -m 发现尚有400M的内存未使用,难道剩余400M的内存还不够go build命令编译代码使用?好吧,既然如此我就给它释放内存,kill掉众多的进程之后再进行go build编译,发现又可以编译了。之后发现偶尔能编译偶尔又不能编译,看样子确实是内存不够,可是为啥之前的centos系统上没有出现这种状况呢,一直不相信简单的“内存不够”就可以解释这一问题,因为之前的centos系统上是正常的,我甚至觉得可能是gcc版本的问题,猜测只有较高版本的gcc才会报这个错误。后来也曾在“golang天朝”论坛上发过帖子,并表达自己的猜测,认为不是内存不够这么简单,结果被别人鄙视不看英文……

不想花钱升级机器硬件,难道我只有装回centos?今天执行free -m偶然间注意到了swap的数值貌似一直是空的,我思考若是我添加swap交换空间是否能解决这一问题呢,毕竟swap其实就是用硬盘空间虚拟出的内存,一个内存的缓冲区。于是就给它加了个1G的文件作为swap,居然直接就可以用go build,再也不用担心gcc对我说Cannot allocate memory了。简单的记录一下添加文件作为swap的步骤:

  • 创建1个1GB的file

sudo dd if=/dev/zero of=/mnt/1GB.swap bs=1M count=1024
  • 格式化为Swap file

sudo mkswap /mnt/1GB.swap
  • 把swap file加入到系统中

sudo swapon /mnt/1GB.swap
  • 将swap永久添加
    在/ect/fstab中加入新的Swap分区

sudo gedit /etc/fstab
  • 在最后加入下列内容

/mnt/1GB.swap none swap sw 0 0

最后,free -m 命令可以看到swap的数据如下

             total       used       free     shared    buffers     cached
Mem:           992        903         88          0         57        188
-/+ buffers/cache:        656        335
Swap:         1023          0       1023

1G的内存交换区文件已经创建。


中文分词逆向最大匹配法项目实践
Tag 中文分词, 逆向最大匹配法, on by view 5732

最近在自己的项目中实现了中文分词,项目是pinyin-php,是一个用C语言完成的原生php扩展。 其主要作用是将中文汉字翻译成汉语拼音,其中用到中文分词主要是为了识别多音字,因为多音字一般是在特定词语环境中出现的。 因此系统需要识别汉字词语,也就是说要实现简单的分词,当然分词准确率越高的话多音字识别率也会越高。

pinyin-php中实现中文分词达到识别多音字的目的,这一想法在pinyin-php上一版本开发的时候就已经想到了。只是自己从来都没有实践过,而且据别人说,中文分词用C语言实现起来比较麻烦,反倒是脚本语言实现起来比较方便。我多方查阅资料后知道现有中文分词方法有如下几种

  1. 字符匹配

  2. 理解法

  3. 统计法

其中字符匹配是基于字典的字符串匹配,理解法是在字符串匹配的同时还对句子进行语义分析,统计法是基于大量的语料库来实现的。其中字符匹配法最为简单,后两者首先是算法比较复杂,是我目前尚未接触到的方法,其次他们均要基于大量的语料库进行数据分析,这对于我以及pinyin-php这个项目来说是不现实的。不过对于搜索引擎这种大型的系统来说是可以实现的。因此,我采用字符匹配法。字符匹配又有如下四种方式

  1. 正向最大匹配法(由左到右的方向);

  2. 逆向最大匹配法(由右到左的方向);

  3. 最少切分(使每一句中切出的词数最小);

  4. 双向最大匹配法(进行由左到右、由右到左两次扫描);

上面四种字符匹配方法要算『双向最大匹配法』最为准确,由于汉语的特性,『逆向最大匹配法』比『正向最大匹配法』要更精确。

pinyin-php采用的便是『逆向最大匹配法』。下面看个分词实例

硕士研究生产

正向最大匹配法的结果会是『硕士研究生 / 产』,而逆向最大匹配法利用逆向扫描,可得到正确的分词结果『硕士 / 研究 / 生产』。

项目中有一个字词库,其实是我自己实现的一个hashtable,系统初始化的时候从字词库中读取汉字和词语以及其对应的汉语拼音。

key,value
一,yī
丁,dīng
七,qī
万,wàn
丈,zhàng
三,sān
上,shàng
下,xià
……
阿Q,āQ
阿爸,ābà
阿鼻,ābí
阿呆,ādāi
阿弟,ādì
阿爹,ādiē
阿斗,ādǒu
阿飞,āfēi
阿哥,āgē
……

假设hashtable中的key的最大长度为10个汉字,那么有一段文字,我首先从尾部开始截取10个汉字,放入buffer{10}中然后在hashtable中查buffer[1~10],查到后就截取后续的10个汉字,未查到的话继续查字符串buffer[2~10],未查到继续查buffer[3~10],直到buffer[10],例如若是在buffer[4~10]的时候查到了,则将buffer[1~3]退回段落的原处。匹配到的词直接总hashtable中读取其拼音如此便翻译出来了,若是查找不到则直接保持为原来的字符不变。

分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。

paragraph								buffer					segments
分词就是将连续的字序列按照一定的规范重新		组合成词序列的过程。
分词就是将连续的字序列按照一定的规范重		新组合成词序列的过程		/。
分词就是将连续的字序列按照一定的规			范重新组合成词序列的		/过程。
分词就是将连续的字序列按照一定的				规范重新组合成词序列		/的/过程。
分词就是将连续的字序列按照一				定的规范重新组合成词		/序列/的/过程。
分词就是将连续的字序						列按照一定的规范重新		/组/合成词/序列/的/过程。
分词就是将连续的							字序列按照一定的规范		/重新/组/合成词/序列/的/过程。

如果想了解我是如何编码实现的,欢迎参阅我的项目pinyin-php(https://github.com/duguying/pinyin-php)。