关于kill进程链
Tag 进程, 子进程, kill, on by view 2808

之前在做 Goj 项目的时候执行服务器部分使用 go 语言 kill 进程,发现无法 kill 该进程的子进程,后来发现貌似别的语言也无法直接 kill 掉当前进程的子进程链。kill 子进程链的应用场景是这样的,我在做 judger服务器的时候发现我调用 gcc 编译源码会在某些极端的情况下卡死,然后卡死后我需要 kill 掉 gcc 的进程,我使用 go 语言的相关 api kill 掉 gcc (因为我拿得到 gcc 的进程 id),然而,我发现 gcc 会调用别的命令,记忆中貌似是一个叫做 ar 的命令,实际上是这个子进程卡死了,于是我 kill 掉身为父进程的 gcc 之后,它的子进程依然存在,变成了孤儿进程,继续卡着。但是我没有 api 可以拿到 gcc 卡死的子进程的进程 id,所以我也无法 kill 掉它。我当时的临时解决方案是调用 killall 命令去按照名称杀掉它。

之后一次在某个群里提到这件事情,随口问了一句如何获取某个进程的子进程,linux api 中是没有这种 api 的,一位同学提醒了我,你可以从 /proc 目录中查。今天我用 go 语言实现了它,linux 系统中 /proc 目录记录了所有的进程信息,但是这些信息中依然没有记录某个信息的子进程,不过它记录了某个进程的父进程,于是我遍历了 /proc 目录中的所有进程的进程信息,然后根据进程的父代关系构成了一颗进程树,这颗树的顶端必定是 init 进程,于是,我便可以从这颗进程树上从上往下搜索查找出某个进程的子进程了。这样一来,一个简单的进程树便可以让我轻松的查找出某进程的子进程,同样也可以 kill 掉某颗进程树,将当前进程及其子进程 kill 掉已经不再是一件难事了。

如果你对此感兴趣,欢迎参阅源码 https://github.com/gojudge/proc 

最后,我要说明的一点是:世界上没有什么事情是一棵树解决不了的,如果有,那么两颗树一定能够解决。


Hibernate中自动建表unique无效的问题
Tag Hibernate, 建表, unique, on by view 3387

使用Hibernate自动建表,User实体中的username字段希望给个唯一约束,但是自动建表却一直没有unique约束

@Entity
@Table(name = "user")
public class User {
	private int id;
	private String username;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	@Column(unique = true, nullable = false)
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

}

键表的结果是索引中并没有唯一索引,多番尝试解决如下

@Entity
@Table(name = "user")
public class User {
	private int id;
	private String username;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	@Column(unique = true, length = 20, nullable = false)
	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

}

原因是我未指定 username 字段的长度,然后自动建表的字段长度是255字符,于是没有唯一索引,将长度指定为 20 ,删除表,重新执行建表便有了唯一索引。另需注意的是 nullable = false 。


浅谈软件众包
Tag 众包, 思考, on by view 2547

互联网在发展,也有众多的思维与新颖的产品逐步产生。譬如国外的 Airbnb, Uber,又如国内的 滴滴打车,这些都是众包这一概念衍生出的产品。近年来,国内又有一些公司开始了尝试软件众包。软件众包的难度却往往不是 Airbnb, Uber, 滴滴打车 这些“众包”产品所能比拟的。因为软件开发的复杂程度远远超出了Airbnb的临时住宿,Uber滴滴打车的租车业务。

要想做好软件众包已经不仅仅是简单的将需求方与开发者联系到一起,有人说 Airbnb 的成功在于将房屋出租者与有住宿需求的人联系到一起,Uber和滴滴打车他们的成功在于将身为有车族的临时司机或专业司机与乘客联系到一起,而做软件众包的人若是觉得他需要做好的仅仅是将软件需求方与开发者联系到一起那么便是大错特错了,因为将软件需求方与开发者联系到一起这种简单的事情是不会产生生产力,软件需求方不懂技术,热衷于技术的众多基础开发者也是不懂得与需求方“周旋”因为他们往往不具备项目经理、产品经理的能力,而缺了这些往往无法成功的将开发者与需求发黏在一起,被开发者吐槽的项目经理产品经理往往是开发者不可缺少的,他们之间存在一种微妙的共生关系。而对于一个项目并不是简单的需求方与开发者碰撞的产物,我个人认为,普通开发者与需求发的碰撞往往只有一个结果——双方都会被对方撞成渣渣。

若是有人觉得我将软件需求方与开发者联系到一起就能够实现软件众包,那么你趁早将“软件众包平台”改名为“软件外包竞标平台”。作为一个资深的开发者(原谅我的装逼),我是没有打算转行做项目经理或是产品经理,我有的是技术,靠技术吃饭,让我做与需求方“周旋”这种事情,那是浪费我写代码研究技术的时间,我不会跟需求方讨价还价也不希望需求方跟我讨价还价,多少钱买多少代码多少功能,只要我觉得合适我就干,少一分也不干。所以,众包平台若是简单的将需求方与开发者联系到一起,绝对会发生碰撞,而且一定会撞得头破血流。

假如我是一个开发者:我心目中的众包一定要有分得足够细致的需求,而且还是明码标价的,完成某个ISSUE给多少钱,而我只需要接收那个我觉得出价合适的ISSUE(或称为细分的需求),在指定的时间内完成便可以拿到上面所标注的价格。接收需求后我所需要做的只是将所有的精力放在我所钟爱的编码上即可。

假如我是一个测试人员:我心目中的众包应该是有一个个的功能模块和一个个的需求供我选择测试,明码标价。按照ISSUE分的测试任务,按照模块分的模块测试任务,甚至是整项目业务逻辑回归测试,安全漏洞悬赏测试。不同粒度的测试任务价格也是不同的,当然,因为我是资深的测试人员,你也可以雇佣我为当前项目的测试顾问,负责维护整个项目的整体可靠性保障。

假如我是一个项目经理……

我不想写那么多假如了,上面的所有假如必须有众多的幕后工作才可能实现,否则一切皆空。

项目经理、首席技术总监,者两种角色便是上面假设成真的保障,谁来跟需求方确立最终的需求,谁来跟需求方“周旋”,这是项目经理所应当做的事情。谁来讲上面假设变为现实,需要将需求拆分为如此细致的开发任务,并且保障整体的有效性与可用性,这些便是技术负责人该项目的首席技术总监所要做的事情。

我不知道我为什么要写这篇文章,或许是在众包项目组待了这么长时间,也或许是别的原因。纵观国内的软件众包平台,根本没有能够达到这个要求的,我也不愿意一一点评。倒是我之前在某在线视频教育平台找的兼职讲师有点这种感觉,他们让我签什么狗屁协议,上面写的如果违约对方有权利罚款5w,我特么当然不签,最终还是他们妥协了,事实如我所料,我并没有太多时间去录制他们要求的“高质量”的视频,干了三课就休业了,这种想干就干,想不干就不干的感觉才是自由职业者所追求的自由。或许有人说,你这么不负责,撒手不干了,岂不是打乱了别人的课程计划?你想多了,从来都不缺会录课会教学的人,即便没人接我的班,我所录的课程也会一直为他们产生价值创造财富,他们甚至会将我所录的三节课编入别的分类体系中。

不管是举例还是装逼,假如我决定做自由职业者的话,这种自由都没有,我特么还是去别人公司老老实实的上班去吧。


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

在公司团队协作用了大半年的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 3367

现代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 3542

一个事物有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 3199

此文不介绍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 2714

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 3091

对于编译安装的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 3538

在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连接服务器试试看,此时需要密码,使用你的新密码便可以登录了。