Linux 常用Yum源

EPEL

EPEL的全称叫 Extra Packages for Enterprise Linux 。EPEL是由 Fedora 社区打造,为 RHEL 及衍生发行版如 CentOS、Scientific Linux 等提供高质量软件包的项目。

官方网址为:https://fedoraproject.org/wiki/EPEL

Centos安装命令: sudo yum install epel-release

Remi repository

Remi repository 是包含最新版本 PHP、MySQL、Python 包的 Linux 源,由 Remi 提供维护。

官方网站为 http://rpms.remirepo.net/

Enterprise Linux 7 (with EPEL) x86_64 的安装方法:

wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
wget https://rpms.remirepo.net/enterprise/remi-release-7.rpm
rpm -Uvh remi-release-7.rpm epel-release-latest-7.noarch.rpm

rpmfusion

RPM Fusion provides software that the Fedora Project or Red Hat doesn't want to ship. That software is provided as precompiled RPMs for all current Fedora versions and current Red Hat Enterprise Linux or clones versions; you can use the RPM Fusion repositories with tools like yum and PackageKit.

RPM Fusion is a merger of Dribble, Freshrpms, and Livna; our goal is to simplify end-user experience by grouping as much add-on software as possible in a single location. Also see our FoundingPrinciples.

官方网站为 https://rpmfusion.org/

Centos8搭建MiniPC/NAS/BLOG笔记

问题汇总

USB安装centos8,无法正常启动安装程序

解决方法: 刻录时,把UltraISO的写入方式改为为RAW

Linux wget遭遇证书不可信(Wget error: ERROR: The certificate of is not trusted.)

解决方法:
安装ca-certificates,并同步系统时间为最新。

  • yum install -y ca-certificates
  • ntpdate -u 0.north-america.pool.ntp.org

Error "curl: (60) Peer certificate cannot be authenticated with known CA certificates"

同上一个问题的解决方法

无法 mount ntfs磁盘

  • yum install epel-release
  • yum install ntfs-3g

umount: /mnt: target is busy.

  • 执行sync把buffer的数据写入磁盘
  • ls /mnt 查看占用/mnt的进程,并kill掉
  • 重新执行 umount /mnt

挂载的数据硬盘设置自动休眠

  • hdparm -B 127 /dev/sdb1
  • hdparm -S 60 /dev/sdb1

设置 supervisord 开机启动

systemctl enable supervisord.service

设置用户 i 可以无密码获得sudo权限

添加 i ALL=(ALL) NOPASSWD: ALL 到 /etc/sudoers

centos 配置无线网卡

参考 https://www.cnblogs.com/asker009/p/10212045.html

安装配置rsyslog

  • sudo yum install rsyslog

查看CPU温度、硬盘温度

  • yum install lm_sensors hddtemp
  • sensors
  • hddtemp /dev/sdb

使用Python把数据库中数据导出到CSV文件

需要安装两个库pymysql和csv

pip install pymysql csv

代码示例如下:

import pymysql
import csv

import sys
reload(sys)
sys.setdefaultencoding('utf8')

def from_mysql_get_all_info():
    conn = pymysql.connect(
            host='mysql.dba.zuocheng.net',
            port=13306,
            user='root',
            db='test_db',
            password='test_pw',
            charset='utf8')
    cursor = conn.cursor()
    sql = "select * from test_table"
    cursor.execute(sql)
    data = cursor.fetchall()
    conn.close()
    return data

def write_csv():
    data = from_mysql_get_all_info()
    filename = 'data_full.csv'
    with open(filename,mode='w') as f:
        write = csv.writer(f,dialect='excel')
        for item in data:
            write.writerow(item)

write_csv()

使用C++11的特性来设计和实现API友好的高精度定时器TimerController

为什么设计和实现TimerController?

最新的TimerController代码保存在Github上面:https://github.com/zuocheng-liu/StemCell ,包含timer_controller.h 和 timer_controller.cpp两个文件,欢迎审阅!

因为软件设计中面临了一些实际问题

尤其在使用C++开发网络应用时常遇到下面的问题:

一、软件设计中,不会缺少通过使用定时器的来实现的场景,比如超时控制、定时任务、周期任务。

二、C/C++标准库中只有最原始的定时接口。没有提供功能完备的库来满足上面提到复杂场景。

三、第三方库中的定时器,往往存在一些问题,比如:

  • libevent、libev、libue 不是线程安全的,在多线程系统中,为了保证线程安全需要额外再进行封装。
  • redis的异步库libae对延时时间的处理是不准确的。

以上问题会让开发者开发新系统时带来一些困扰,但C++11新特性的出现,带来了解决上面问题的新思路。

C++11的新特性让定时器的实现更简单友好

TimerController接口更友好

接口参数支持C++11的lamaba表达式,让定时器的接口对开发人员更加友好。

代码更精简

TimerController的代码总计300~400行,而且功能完备,代码健壮。
C++11的新特性的使用,让代码更简洁,增强代码的可读性、可维护性。

保证线程安全

线程安全,是绕不开的问题。第三方库libevent等,在多线程环境中使用总是危险的。TimerController在设计之初就保证多线程环境下运行的安全性。

没有第三方依赖。

TimerController,没有依赖任何第三方库,完全依靠C/C++标准库和C++11的新特性来实现。

TimerController 接口设计

class TimerController {
    bool init(); // 初始化资源,并启动定时器
    void stop(); // 停止定时器,所有定时任务失效

    // 定时运行任务,参数delay_time单位是毫秒。后面参数是lamba表达式
    template<class F, class... Args>
    void delayProcess(uint32_t delay_time, F&& f, Args&&... args);

    // 周期运行任务,参数interval单位是毫秒,后面参数是lamba表达式。
    template<class F, class... Args>
    void  cycleProcess(uint32_t interval, F&& f, Args&&... args);
}

用一个实例来讲解TimerController的使用方法:

#include <iostream>
#include "timer_controller.h"
using namespace std;
using namespace StemCell;
int main() {
    srand((unsigned)time(NULL));
    try {
        TimerController tc;
        tc.init(); // 初始化 TimerController
        tc.cycleProcess(500, [=]() { cout << "cycle 0.5 sec" << endl; });
        for (int i = 0; i < 80; ++i) {
            // 随机产生80个延时任务,并延时执行
            auto seed = rand() % 8000;
            tc.delayProcess(seed + 1, [=]() { cout << "delay:" << seed << endl; });
        }
        sleep(8);  // 主线程睡眠8秒,让延时任务得以执行
        tc.stop(); // 停止 TimerController
        cout << "tc stoped!" << endl;
    } catch (exception& e) {
        cout << "error:" << e.what();
    }
    return 0;
}

TimerController 实现原理

TimerController如何实现定时的原理和libevent并无不同,都使用了Reactor的设计模式。但是TimerController通过生产者、消费者模型,来实现多线程添加定时任务,并保证线程安全。TimerController使用了C++11的新特性,简化了代码实现。

  • 使用最小堆来管理延时任务和周期任务
  • 使用1个timerfd配合epoll来实现定时任务的激活
  • 使用1个eventfd配合epoll来实现定时任务的添加
  • 使用一个线程安全的队列,实现生产者消费者模型。TimerController使用场景为多线程写,TimerController内部1个线程来读。

TimerController 高级用法

高延时的任务的处理

TimerController内部只有1个线程在执行定时任务。当高延时的任务增多时,可能会影响到任务运行的调度时间,高延时的任务需要在放入新的线程中运行。示例如下:

TimerController tc;
tc.init(); // 初始化 TimerController
// 把任务放入新线程或线程池中
tc.delayProcess(50, []() { std::thread([]() { do_long_time_task();}) });

TimerController保持全局单例

为了系统简洁,TimerController全局单例即可。
auto& tc = Singleton< TimerController >::GetInstance();

其他

如何避免CPU负载高时,定时器失准的问题?

TimerController 有待改进的点

  • 无锁化,目前使用了自旋锁在保证task队列的线程间互斥,后续可使用无锁队列替代有锁队列。
  • TimerController精度目前只有1毫秒,主要因为博主做网络开发都是毫秒级的,后续可以让TimerController支持更小的精度。
  • TimerController 使用了epoll、timerfd、eventfd等,只能在linux平台上面使用

源码地址

具体实现在 timer_controller.h 和 timer_controller.cpp两个文件里面。

使用docker搭建C/C++ 开发环境

优点和意义

  • 环境隔离、资源共享、节省机器资源
  • 轻量虚拟机,启动和运行迅速

使用 Dockerfile 构造镜像

Dockerfile 内容

FROM centos:centos7.4.1708

MAINTAINER Zuocheng Liu <zuocheng.liu@gmail.com>

RUN yum -y --nogpgcheck install gcc gcc-c++ kernel-devel make cmake  libstdc++-devel libstdc++-static \
&& yum -y --nogpgcheck install openssl-devel gperftools-libs \
&& yum -y --nogpgcheck install psmisc openssh-server sudo epel-release \
&& yum -y --nogpgcheck install vim git ctags \
&& mkdir /var/run/sshd \
&& echo "root:123456" | chpasswd \
&& sed -ri 's/^#PermitRootLogin\s+.*/PermitRootLogin yes/' /etc/ssh/sshd_config \
&& sed -ri 's/UsePAM yes/#UsePAM yes/g' /etc/ssh/sshd_config \
&& ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key \
&& ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key

EXPOSE 22

CMD ["/usr/sbin/sshd", "-D"]

创建镜像

docker build -t cpp_dev:last .

创建容器

初始化容器, 宿主机调用脚本

  • 挂载容器
  • 调用容器内初始化脚本mount_in_docker.sh
#!/bin/bash

NEWUSER=$1
PORT=$2
PATH=$(dirname $(readlink -f "$0"))
echo "add User ${NEWUSER} sshd Port ${PORT}"
if [ ! -d /data/rtrs/${NEWUSER} ]; then
    /usr/bin/mkdir /data/rtrs/${NEWUSER}
fi
/usr/bin/cp /data/liuzuocheng/.profile /data/${NEWUSER}/.profile
/usr/bin/docker run -itd --name ${NEWUSER} --net=host -v /data/${NEWUSER}:/home/${NEWUSER} -v /data:/data rtrs/dev_cpp:centos7.4.1708
/usr/bin/docker exec -i ${NEWUSER} sh ${PATH}/mount_in_docker.sh ${NEWUSER} ${PORT} ${UID}

镜像内调用脚本

  • 添加用户
  • 启动sshd服务
NEWUSER=$1
PORT=$2
NEWUID=$3
echo "add User ${NEWUSER} sshd Port ${PORT}"
/usr/sbin/useradd -r -u ${NEWUID} -d /home/${NEWUSER} ${NEWUSER}
echo "${NEWUSER}:123456" | /usr/sbin/chpasswd
echo "Port ${PORT}" >> /etc/ssh/sshd_config
/usr/sbin/sshd

其他问题汇总

宿主机docker的目录迁移

docker的默认目录在/var/lib/docker 下面, 往往/var的磁盘比较小,建议把docker的目录改为大磁盘

把用户加入sudoer列表,sudo 执行命令无需密码

修改/etc/sudoers文件, 添加一行 user-name ALL=(ALL) NOPASSWD: ALL

更多问题可参考《Docker使用经验》

现实过程中使用docker搭建C/C++ 开发环境的工程意义

  • 新人入职代码培训,降低学习成本
  • 推动公司平台化建设

Docker使用经验

安装docker

ubuntu 环境

sudo apt-get install docker.io
sudo systemctl start docker

centos环境

sudo yum install docker
sudo service docker start

常用命令

启动docker

sudo docker run --net=host -v /home/zuocheng:/zuocheng -it zuocheng/dev_cpp /bin/bash

创建docker镜像

docker build -t zuocheng/dev_cpp .

挂载镜像

docker run -it --name dev_cpp -p 10022:22 zuocheng/dev_cpp

要免 sudo 调用 docker 命令方法, 创建docker用户组,并把当前用户添加到docker 用户组中。

sudo groupadd docker #创建 docker 用户组

sudo usermod -aG docker ${USER} # 当前用户加入 docker 用户组

sudo systemctl restart docker # 重启 docker 服务

newgrp - docker # 使设置在当前会话生效

使用docker搭建C++开发环境

参考博文《使用docker搭建C/C++ 开发环境》

更改docker的存储目录,避免默认目录磁盘不足的问题

vim /usr/lib/systemd/system/docker.service # 修改docker.service文件.

ExecStart=/usr/bin/dockerd --graph /data/docker # 在里面的EXECStart的后面

重启docker

systemctl disable docker
systemctl enable docker
systemctl daemon-reload
systemctl start docker

向量检索-大规模向量检索引擎

梗概

近期在团队内分享之前在阿里巴巴达摩院工作时做过的项目。在这次分享中,简单介绍了向量检索的原理,以及在大规模数据下如何构建一个高性能、低延迟的向量检索引擎。

更详细的信息在PPT 《向量检索-大规模向量检索引擎》里。

脑图

向量检索-大规模向量检索引擎

系统架构图和离线数据流图

大规模向量检索引擎-架构图

其他资料

使用C语言实现的最简单的HTTP服务器

此段代码的特点

  • 功能简单,无论请求是什么,应答只返回<h1>Hello!</h1>
  • 代码量小,总计68行。
  • 可编译、可运行。

如何编译运行?

编译: gcc -o hello_server hello_server.c

运行: ./hello_server

请求: curl http://localhost:8888/any

源文件 hello_server.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define PORT 8888
#define BUFFER_SIZE 4096
#define RESPONSE_HEADER "HTTP/1.1 200 OK\r\nConnection: close\r\nAccept-Ranges: bytes\r\nContent-Type: text/html\r\n\r\n"
#define RESPONSE_BODY "<h1>Hello!</h1>"

int handle(int conn){
    int len = 0;
    char buffer[BUFFER_SIZE];
    char *pos = buffer;
    bzero(buffer, BUFFER_SIZE);
    len = recv(conn, buffer, BUFFER_SIZE, 0);
    if (len <= 0 ) {
        printf ("recv error");
        return -1;
    } else {
        printf("Debug request:\n--------------\n%s\n\n",buffer);
    }

    send(conn, RESPONSE_HEADER RESPONSE_BODY, sizeof(RESPONSE_HEADER RESPONSE_BODY), 0);
    close(conn);//关闭连接
}

int main(int argc,char *argv[]){
    int port = PORT;
    struct sockaddr_in client_sockaddr;     
    struct sockaddr_in server_sockaddr;
    int listenfd = socket(AF_INET,SOCK_STREAM,0);
    int opt = 1; 
    int conn;
    socklen_t length = sizeof(struct sockaddr_in);
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(port);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(bind(listenfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error!\n");
        return -1;   
    }  

    if(listen(listenfd, 10) < 0) {
        printf("listen failed!\n");
        return -1;   
    }

    while(1){
        conn = accept(listenfd, (struct sockaddr*)&client_sockaddr, &length);
        if(conn < 0){
            printf("connect error!\n");
            continue;
        }
        if (handle(conn) < 0) {
            printf("connect error!\n");
            close(conn);
            continue;
        }  
    }
    return 0;
}

后记

为什么要写这篇博文?

原因是,在使用公司里的自动化平台部署c++服务时,拿这个简单的示例来测试平台是否有问题。俗称趟一遍坑儿。

在网上也搜索了很多不少博文,发现里面的代码有几个问题,第一个问题就是编译不过,第二个问题则是有的代码应答必须要有文件,这对我的测试也造成了些许麻烦。

所以就自己参考别人的列子,在自己的博客里写一个简单的吧。以后再去趟别的自动化部署系统的坑的时候,顺手就能拿来用。

sed 使用经验

概述

sed 是处理文本时经常用到的工具,可实现对文本的输出,删除,替换,复制,剪切,导入,导出等各种操作。

本文依据作者的自身经验,记录sed的常用的使用方法。本文虽然不能涵盖sed的所有功能,但本文记录的sed用法能够解决了作者日常工作中 90%以上的问题。希望通过本文的分享,能让读者从sed的字数繁多的说明文档中解救出来,减小学习sed的成本。

sed 常用用法

sed -i 's/regular_expression/replacement/g' file.txt

这个命令格式是最常用的用法,在工作中使用sed场景中出现的频率最高。

对参数和各部分说明:

-i 表示修改文件,如果不带此参数,则将匹配的结果打印到屏幕。

s 表示替换字符串

regular_expression 正则表达式

replacement 对正则表达式匹配的目标,进行替换。如果replacement为空

g 对文件全局做替换

举例:

[www@it.zuocheng.net]$ sed -i 's/[a-z]*/+++/g' file.txt

sed 注意事项

在sed的正则表达式中, 有一些需要转移的特殊字符,比如:

+、(、),这些字符如果是正则表达式的语法中使用的字符,则需要进行转移;如果是文本中需要匹配的字符,则不需要转义。

高级用法

& 变量的使用

& 变量用在replacement部分,表示regular_expression匹配到的文本。

[www@it.zuocheng.net]$ echo "hello world"| sed 's/[a-z]*/(&)/g'
(hello) (world)

正则匹配 \1 - \9 变量

使用sed提取正则匹配的各个部分。

正则匹配的变量,主要用在replacement部分。表示regular_expression的子变量。

可用于截取字符串

[www@it.zuocheng.net]$ echo "hello123 world456"| sed 's/\([a-z]\+\)[0-9]\+/\1/g'
hello world

图搜引擎服务器磁盘IO性能测试方案

背景和意义

目前团队对图搜业务使用的服务器的磁盘IO性能没有量化的数据,不能准确把握服务器的磁盘IO性能。

同机型机器的磁盘IO性能表现并不相同,在磁盘IO性能表现差的机器,往往存在故障隐患。

因为不能准确把握服务器的IO性能,对图搜引擎能否支持多少业务量时,不能做出准确评估。在运维团队系统的过程中,会存在不可预知的风险,最终导致系统故障。

机器预算收紧,把索引全部加载到内存的方案暂时无法推行,在图搜引擎检索过程中引依然有磁盘IO操作。对磁盘IO测试仍有重要意义。

测试目标

  • 测试现有机型机器的磁盘IO性能(日常、极限)。
  • 对测试新型存储介质的性能,建立测试方法。
  • 针对图搜业务,建立服务器磁盘IO性能基线标准。

有了磁盘IO性能基线标准,有如下好处:

  • 在系统扩容的过程中,对加入集群的新机器,做磁盘IO性能准入测试,降低线上环境的硬件故障发生率。
  • 以后引入新型服务器,在评估其性能时,做对比和参照。
  • 在搭建新的图搜服务,预估系统可承受的最大业务量时,可以做为参考依据。

测试方法

测试方法分析

图搜搜索引擎磁盘IO读写特点

对于图搜搜索引擎,主要有三种IO操作,分别是随机读、顺序写和顺序读。

  • 随机读

图搜引擎在提供线上服务时,磁盘IO操作主要来自于读取磁盘中存储的正排字段, 随机存储于正排索引文件当中。

  • 顺序写和顺序读

顺序写和顺序读,分别为索引同步和索引加载这两个过程中的磁盘IO读写特点,频率为1天各执行1次。

索引文件打开方式主要为mmap

测试注意点

针对图搜引擎操作磁盘时的IO特点,在做磁盘IO性能测试时需要注意下面几点:

  • 测试随机读、顺序写和顺序读三种读写情况,重点是测试随机读的磁盘IO性能。

  • 随机读测试中,文件大小要到500G,并做96路并发测试。每次读写块的大小为4k

  • 要使用mmap方式打开文件。

  • 在测试的过程中,要排除linux cache机制的影响,只对磁盘IO性能进行测试。

测试指标

重点关注在3种磁盘读写方式(随机读、顺序写和顺序读)下的IO指标:

名称备注
BW平均IO带宽
IOPSInput/Output Operations Per Secondlatency响应时间complete latency完成延迟
CPU CPU使用率

测试工具

FIO 2.1.10
top/vmstat

测试步骤

测试步骤注意要点

  • 相同的测试必须测试3遍,并记录下每次的测试结果数据。
  • 每组性能测试的时间不能少于60秒。
  • 测试前检查服务器上的应用进程,关闭无关进程,减少无关影响。

具体测试步骤

以测试服务器 x.x.x.x 的磁盘IO性能为例:

  • 关闭所有无关进程。
  • 下载编译和安装FIO。
  • 编写fio job文件
  • 此fio job文件,包含3个job,分别是随机读、顺序读、顺序写。
  • 执行测试命令
    ./fio fio_jobs.ini

测试结果如下:

  • 解读并记录测试结果。
  • 测试结果分析方法
  • 对照手册,解读FIO的测试结果。

    如何建立图搜引擎服务器磁盘IO性能基线标准

对目前拍立淘图搜引擎使用的机型,依据本文的方法做性能测试。并统计磁盘IO性能测试结果。

对各个机器磁盘IO性能测试结果中,剔除误差数据,取中位统计结果做平均值为IO性能基线标准。

参考资料

汉明距离之算法和实现总结

内容简介

汉明距离,通过比较向量每一位是否相同,求出不同位的个数。用来表示两个向量之间的相似度。

汉明距离计算的步骤,即对两个向量首先进行异或操作,然后对异或的结果的每一位bit进行统计,最后合计出有多少bit的值为1。

本文主要内容为,列举出9种计算汉明距离的算法及其C++代码的实现,并使用本文的测试方法测试得出不同算法的性能。再对不同的算法进行分析比较。

计算机在进行异或操作中,CPU的指令集可以提供多种实现。比如cpu固有指令 xor 和 SSE指令集 、AVX指令集。后两种指令集都是为了提升CPU对向量的处理速度而扩展的指令集。

汉明距离计算的后一步,计算一个变量中所有bit的值为1的个数。可以使用多种算法和实现方式来实现。比如算法上,逐位移位统计、查表法、分治法等;实现方式上,可以使用前面所说的算法,也可以使用cpu的指令popcnt直接求得。

实现算法

本文算法计算的向量为二值向量;

本文中包含算法函数的Algorithm类声明如下,不同计算汉明距离的算法的类都继承这个基类,计算汉明距离由成员函数 uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size)实现,函数输入为3个参数,其中p和q分别为两个向量的指针;size为向量维度的大小,并以64为倍数; 向量最小为64个bit。

class Algorithm {
  public:
    ~Algorithm() {}
    virtual void init() {}
    virtual std::string getName() {
      return "Undefined Algorithm Name.";
    }
    virtual uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) = 0;
};

一般算法

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      for (uint64_t i = 0; i < size; ++i) {
        uint64_t r = (*(p + i)) ^ (*(q + i));
        while (r) {
          res += r & 1;
          r = r >> 1;
        }
      }
      return res;
    }

使用gcc内建函数优化一般算法

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      for (uint64_t i = 0; i < size; ++i) {
        uint64_t r = (*(p + i)) ^ (*(q + i));
        res += __builtin_popcountll(r);
      }
      return res;
    }

查表法-按8bit查询

class HammingDistanceTable8Bit : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceTable8Bit";
    }
    void init() {
      pop_count_table_ptr = NULL;
      pop_count_table_8bit_init(&pop_count_table_ptr);
    }
    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      for (uint64_t i = 0; i < size; ++i)
      {
        uint64_t r = (*(p + i)) ^ (*(q + i));
        res += pop_count_table_8bit(r);
      }
      return res;
    }
  private:
    uint8_t *pop_count_table_ptr; 
    void pop_count_table_8bit_init(uint8_t **pop_count_table_ptr) {
      *pop_count_table_ptr = new uint8_t[256];
      for (int i = 0; i < 256; ++i) {
        (*pop_count_table_ptr)[i] = __builtin_popcount(i);
      }  
    }

    uint64_t pop_count_table_8bit(uint64_t n) {
      int res = 0;
      uint8_t *p = (uint8_t *)&n;
      for (int i = 0; i < 8; ++i) {
        res += pop_count_table_ptr[*(p + i)];
      }
      return res;
    }
};

查表法-按16bit查询

class HammingDistanceTable16Bit : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceTable16Bit";
    }
    void init() {
      pop_count_table_ptr = NULL;
      pop_count_table_16bit_init(&pop_count_table_ptr);
    }

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      for (uint64_t i = 0; i < size; ++i)
      {
        uint64_t r = (*(p + i)) ^ (*(q + i));
        res += pop_count_table_16bit(r);
      }
      return res;
    }
  private:
    uint8_t *pop_count_table_ptr; 

    void pop_count_table_16bit_init(uint8_t **pop_count_table_ptr) {
      *pop_count_table_ptr = new uint8_t[65536];
      for (int i = 0; i < 65536; ++i) {
        (*pop_count_table_ptr)[i] = __builtin_popcount(i);
      }  
    }

    uint64_t pop_count_table_16bit(uint64_t n) {
      int res = 0;
      uint16_t *p = (uint16_t *)&n;
      for (int i = 0; i < 4; ++i) {
        res += pop_count_table_ptr[*(p + i)];
      }
      return res;
    }
};

分治法

class HammingDistanceDivideConquer : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceDivideConquer";
    }

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      for (uint64_t i = 0; i < size; ++i)
      {
        uint64_t r = (*(p + i)) ^ (*(q + i));
        res += pop_count_divide_conquer(r);
      }
      return res;
    }

    uint64_t pop_count_divide_conquer(uint64_t n) {
      n = (n & 0x5555555555555555) + ((n >> 1 ) & 0x5555555555555555); 
      n = (n & 0x3333333333333333) + ((n >> 2 ) & 0x3333333333333333);
      n = (n & 0x0F0F0F0F0F0F0F0F) + ((n >> 4 ) & 0x0F0F0F0F0F0F0F0F);
      n = (n & 0x00FF00FF00FF00FF) + ((n >> 8 ) & 0x00FF00FF00FF00FF);
      n = (n & 0x0000FFFF0000FFFF) + ((n >> 16) & 0x0000FFFF0000FFFF);
      n = (n & 0x00000000FFFFFFFF) + ((n >> 32) & 0x00000000FFFFFFFF);
      return n;
    }
  private:

};

改进分治法

class HammingDistanceDivideConquerOpt : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceDivideConquerOpt";
    }

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      for (uint64_t i = 0; i < size; ++i)
      {
        uint64_t r = (*(p + i)) ^ (*(q + i));
        res += pop_count_divide_conquer_opt(r);
      }
      return res;
    }

    uint64_t pop_count_divide_conquer_opt(uint64_t n) {
      n = n - ((n >> 1)  & 0x5555555555555555); 
      n = (n & 0x3333333333333333) + ((n >> 2 ) & 0x3333333333333333);
      n = (n + (n >> 4 )) & 0x0F0F0F0F0F0F0F0F;
      n = n + (n >> 8 );
      n = n + (n >> 16);
      n = n + (n >> 32);
      return (uint64_t)(n & 0x7F);
    }
  private:

};

使用SSE指令集

class HammingDistanceSSE : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceSSE";
    }

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      uint64_t temp_res[2] = {0, 0};
      for (uint64_t i = 0; i < size; i += 2) { 
        __m64 *x1 = (__m64*)(p);
        __m64 *x2 = (__m64*)(p + 1);
        __m64 *y1 = (__m64*)(q);
        __m64 *y2 = (__m64*)(q + 1);
        __m128i z1 = _mm_set_epi64 (*x1, *x2);
        __m128i z2 = _mm_set_epi64 (*y1, *y2);
        __m128i xor_res =  _mm_xor_si128(z1 , z2);
        _mm_store_si128((__m128i*)temp_res, xor_res);
        res += _mm_popcnt_u64(temp_res[0]);
        res += _mm_popcnt_u64(temp_res[1]);
      }
      return res;
    }
};

使用AVX2指令集

class HammingDistanceAVX : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceAVX";
    }

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      uint64_t temp_res[4] = {0, 0, 0, 0};
      for (uint64_t i = 0; i < size - 1; i += 4) { 
        long long int *x1 = (long long int*)(p + i);
        long long int *x2 = (long long int*)(p + i + 1);
        long long int *x3 = (long long int*)(p + i + 2);
        long long int *x4 = (long long int*)(p + i + 3);
        long long int *y1 = (long long int*)(q + i);
        long long int *y2 = (long long int*)(q + i + 1);
        long long int *y3 = (long long int*)(q + i + 2);
        long long int *y4 = (long long int*)(q + i + 3);
        __m256i z1 = _mm256_set_epi64x (*x1, *x2, *x3, *x4);
        __m256i z2 = _mm256_set_epi64x (*y1, *y2, *y3, *y4);
        __m256i xor_res =  _mm256_xor_si256(z1 , z2);
        _mm256_store_si256((__m256i*)temp_res, xor_res);
        res += _mm_popcnt_u64(temp_res[0]);
        res += _mm_popcnt_u64(temp_res[1]);
        res += _mm_popcnt_u64(temp_res[2]);
        res += _mm_popcnt_u64(temp_res[3]);
      }
      return res;
    }
};

使用AVX512指令集

截止本文完成时,市场上支持avx-512指令的cpu并没有普及,但是gcc已经提供了avx-512的c/c++ 语言接口。本文先把代码实现,后续购得支持avx-512指令的cpu后,再进行测试。

class HammingDistanceAVX : public Algorithm {
  public:
    std::string getName() {
      return "HammingDistanceAVX";
    }

    uint64_t cal(const uint64_t* p, const uint64_t* q, const uint64_t size) {
      uint64_t res = 0;
      uint64_t temp_res[8] = {0, 0, 0, 0, 0, 0, 0, 0};
      for (uint64_t i = 0; i < size - 1; i += 8) { 
        long long int *x1 = (long long int*)(p + i);
        long long int *x2 = (long long int*)(p + i + 1);
        long long int *x3 = (long long int*)(p + i + 2);
        long long int *x4 = (long long int*)(p + i + 3);
        long long int *x5 = (long long int*)(p + i + 4);
        long long int *x6 = (long long int*)(p + i + 5);
        long long int *x7 = (long long int*)(p + i + 6);
        long long int *x8 = (long long int*)(p + i + 7);
        long long int *y1 = (long long int*)(q + i);
        long long int *y2 = (long long int*)(q + i + 1);
        long long int *y3 = (long long int*)(q + i + 2);
        long long int *y4 = (long long int*)(q + i + 3);
        long long int *y5 = (long long int*)(q + i + 4);
        long long int *y6 = (long long int*)(q + i + 5);
        long long int *y7 = (long long int*)(q + i + 6);
        long long int *y8 = (long long int*)(q + i + 7);
        __m512i z1 = _mm512_set_epi64x (*x1, *x2, *x3, *x4, *x5, *x6, *x7, *x8);
        __m512i z2 = _mm512_set_epi64x (*y1, *y2, *y3, *y4);
        __m512i xor_res =  _mm512_xor_si512(z1 , z2);
        _mm512_store_si512((void*)temp_res, xor_res);
        res += _mm_popcnt_u64(temp_res[0]);
        res += _mm_popcnt_u64(temp_res[1]);
        res += _mm_popcnt_u64(temp_res[2]);
        res += _mm_popcnt_u64(temp_res[3]);
        res += _mm_popcnt_u64(temp_res[4]);
        res += _mm_popcnt_u64(temp_res[5]);
        res += _mm_popcnt_u64(temp_res[6]);
        res += _mm_popcnt_u64(temp_res[7]);
      }
      return res;
    }
};

性能测试

测试代码框架类:

class AlgorithmBench {
  public:
    void init() {
      startTimer();
      vector = new uint64_t[size];
      ifstream f("sample.txt", ios::in);
      for(int i = 0; i < size;i++ ) {
        uint64_t c;
        f >> vector[i];
      }
      stopTimer();
      getInterval("load sample cost:");
      f.close();
    }
    void setSize(uint64_t size) { this->size = size;};
    void push_back(Algorithm* algorithm) { _algorithm_vector.push_back(algorithm);}

    void start() {
      for (std::vector<Algorithm*>::iterator iter = _algorithm_vector.begin();
          iter != _algorithm_vector.end();
          ++iter) {
        Algorithm* ptr = *iter;
        ptr->init();
        startTimer();
        for (int i = 0; i < size - 3; ++i) {
          ptr->cal(vector + i, vector + i + 1, 4);
        }
        stopTimer();
        getInterval(ptr->getName() + " cost:");
      }
    }

    void startTimer() {
      gettimeofday(&tv,NULL);
      start_timer = 1000000 * tv.tv_sec + tv.tv_usec;
    }
    void stopTimer() {
      gettimeofday(&tv,NULL);
      end_timer = 1000000 * tv.tv_sec + tv.tv_usec;
    }
    void getInterval(std::string prefix) {
      std::cout<<std::left<<setw(40) << prefix 
        << std::right << end_timer - start_timer<<endl;
    }

  private:
    uint64_t size;
    uint64_t *vector;
    timeval tv;
    uint64_t start_timer;
    uint64_t end_timer;
    std::vector<Algorithm*> _algorithm_vector;
};

编译指令:

g++ -msse4.2 -mavx2 -O2 -o test_hamming hamming_distance.cpp

windows-gcc 测试结果

测试环境:

Windows 7
gcc version 7.4.0

HammingDistanceBase cost:               330066
HammingDistanceBuildin cost:            326566
HammingDistanceTable8Bit cost:          2381976
HammingDistanceTable16Bit cost:         1435287
HammingDistanceDivideConquer cost:      1215243
HammingDistanceDivideConquerOpt cost:   1226745
HammingDistanceSSE cost:                972695
HammingDistanceAVX cost:                680636

Linux-gcc 测试结果

测试环境:

Ubuntu Server 19.04
gcc version 8.3.0

测试结果:

load sample cost:                       78070
HammingDistanceBase cost:               145393
HammingDistanceBuildin cost:            75905
HammingDistanceTable8Bit cost:          598789
HammingDistanceTable16Bit cost:         142502
HammingDistanceDivideConquer cost:      343414
HammingDistanceDivideConquerOpt cost:   316748
HammingDistanceSSE cost:                59322
HammingDistanceAVX cost:                115784

总结

不同平台,不同的编译器版本,测试的结果有所差异。但整体表现上使用SSE指令集的性能最好,其次是使用内建函数计算popcnt性能最优。AVX指令性能略好于SSE。性能最差的是查表法8bit。分治法性能居中。

附录

所有代码都存放在github上面: https://github.com/zuocheng-liu/code-samples/blob/master/algorithm/hamming_distance.cpp

后台服务底层网络通信框架设计方案推演

内容梗概

  • 从简单到复杂,推演后台服务的底层通信框架的进化过程,包括网络IO模型、多线程模型的选择和组合。
  • 推演的最基础模型是IO同步阻塞+单线程模型,然后逐步进化。推动网络框架进化的3个因素是,每秒请求量的增大,并发量的增大,逻辑计算量的增大。
  • 本文使用的底层通信协议以TCP为基础,因此所有推演方案不考虑适用于UDP的信号驱动模型和适用于于文件操作的异步IO模型。
  • 只考虑同步阻塞、同步非阻塞、IO多路复用。
  • 本文不讨论多进程和多线程的区别,在提高cpu利用率上,这两个模型的作用是一致的,因此本文只选择多线程模型进行讨论。
  • 本文主要讨论单机网络通信框架的设计演化,不考虑分布式场景下。本文各项性能对比指标也仅指单台服务器的性能。

推演之前,需要一些系统相关数据支撑。

现有的硬件条件,2017年:

  • 单台服务器处理网络连接请求数,每秒约10W量级。
  • 单机并发处理网络长连接数上限在10W左右(C100K)。
  • CPU 单核,3000MHz,一台服务器以24核计,单核每秒亿次运算。
  • 抛离单次请求的网络处理过程,单核每秒处理每个请求的业务计算量在0-10000W之间。

最简单的网络通信模型, 同步IO阻塞+单线程

此模型是我们推演的基础模型。

适用场景,最简单的网络请求、处理和返回。每秒处理请求量低,并发处理请求数只有1,计算量小。系统硬件、网络IO都不会构成瓶颈。

实例,各类demo、模拟后台服务的测试服务、大型系统中开发的调试接口、数据接口、监控接口。

开始推演:

  1. 假如处理单个请求的计算量不变(依然很小),但请求量增大,并发量增大,网络IO成为瓶颈,这种模型是不能满足需求的。因此需要使用 IO多路复用 + 单线程模型

  2. 假如 请求量、并发量不变,但是处理请求计算量变大,单核CPU成为瓶颈,这种模型也是不能满足需求的。此时需要使用 IO阻塞 + 多线程模型,利用CPU多核提高计算能力。

  3. 假如请求量、并发量变大,而且处理单个请求的计算量也变大,这种模型更是不能满足需求,但此种情况比较复杂,下面需要详细论述。不过一般情况下也可以使用 IO多路复用 + 多线程模型

IO同步阻塞 + 多线程

使用这种模型,则是计算量变大,单核CPU往往成为瓶颈,必须使用多核来提高计算能力,但并发度低。数据举例,24核CPU处理每秒处理请求数小于1W,并发度小于24,请求量小于1000/s。

实例,各类 FastCGI 后台服务、php-fpm,用于机器学习模型计算的服务,图像处理服务。

开始推演:

IO同步阻塞 + 多线程,并发度受限于线程数,不适合处理并发,一旦并发量变高,则网络模型应该改用IO多路复用。

IO多路复用 + 单线程

使用这种模型,请求量大,并发量大,但处理每个请求的计算量小。数据举例,qps 5W以上,并发数高,但单核cpu每秒处理也在5W以上。

实例, redis和memcache的网络模型。

IO多路复用 + 多线程

经过上面的推演,IO多路复用 + 多线程模型应该是推演过程的终点。既能处理大量请求,又能提升并发度,提高CPU的利用率解决计算量大的问题。

实例, 大型网络应用。

总结

无论选择什么样的模型,最终的目的就是提高服务器硬件的利用率,并避免资源浪费。

选择合适模型,必须依据其所在的业务场景,根据请求量、并发量、计算量这个3个指标,选择合适的模型。

问题总结

  1. 为什么不是所有情况都选择IO多路复用 + 多线程模型,IO多路复用 + 多线程解决了高访问量、高并发、计算量大的业务?

主要是因为在一些非高访问量、非高并发、非计算量大的业务场景下,IO多路复用 + 多线程是一种过度设计,容易造成资源浪费。

  1. 为什么同步IO非阻塞并没有在推演过程中使用?

    非阻塞的编码,会让代码逻辑复杂,一般不会使用。

Memcache源代码阅读总结

Memcache的组成部分

从设计的层面讲,Memcache的基本组成元素只有3个:

  • 网络IO多路复用, 由libevent库来支持
  • 内存存储,使用Slab的数据结构 和 LRU内存回收算法,管理内存。
  • Hash算法索引数据,使用链表法来存储hash node/item和解决hash冲突。

Memcache 为什么这么设计?

可以拆解为下面的问题:

  • 为什么网络IO多路复用模型?
  • 为什么要用Hash算法?
  • 为什么要使用Slab的内存管理方法?

为了解答上面的问题,首先要探讨Memcache业务场景的特点

Memcache 的使用场景都是被作为分布式缓存

  • 请求量大,连接数多
  • 单次请求数据小,作为缓存消耗的存储空间小
  • 请求延时要求极小

正是以上特点:

  • IO多路复用用于解决请求量大,连接数多的问题
  • 内存存储数据,读写快,解决延时小的问题,并且满足分布式缓存的需求
  • 使用hash索引数据,则提升数据查询速度,更能满足请求延迟小的要求。

这样设计的好处是,既能满足业务场景的需要,又能有很高的性能。

问题汇总

有没有比memcache更好的设计了?

网络模型可以解除libevent依赖,直接使用epoll。 代码质量可以进一步优化。

memcache 选择单线程还是多线程?

早期memcache使用单线程,后期换为多线程。

为什么高版本的memcache选择了多线程模型?

突破单核cpu计算瓶颈,增强并发能力。但我认为提升成本大,效果小,单线程更合理些。

广告引擎技术特点

是一种搜索引擎

广告引擎,是适用于广告搜索场景下的搜索引擎。广告引擎特点,也是由广告业务需求所决定的。

流量大

  • 依附于大流量的用户产品之中,负责将用户流量转化为商业流量。
  • 用户量大,但客户数广告数少。
  • 虽然流量大,但是对并发度的要求比较低。和用户产品相比,广告引擎的业务场景往往不需要和客户端维持长连接。

低延迟

  • 信息流广告必须快于信息,才能不伤害用户体验。
  • 特殊场景广告,必须低延迟,比如APP的开屏、插屏,不然会影响客户使用APP。
  • 在RTB系统中,对低延迟的要求更高。

一方面,由于exchange的存在,增加了一次网络请求和中间的竞价过程,DSP系统的响应时间就要更加短;另一方面,exchange对于DSP的响应时间会作为二次竞价的权值,提高响应速度对提高收入有利。

检索数据量少

  • 业界广告主数量少, 100万~1000万量级
  • 广告物料也少,1亿量级

和传统的搜索引擎相比,广告引擎的数据量并不大。

生产的数据量大

  • 用户每次的访问、展示打点追踪、点击打点追踪都要被记录,每日产生的数据按TB计算。

高可用

虽说高可用几乎成了所有后台服务的特点,但是广告引擎对高可用的要求尤其要高。

  • 系统的每次故障,都可能会造成公司收入的直接降低。

用户的产品的后台系统一旦出现故障,可能损失是用户数,或者伤害了用户体验,但是广告系统损失的是最直接的收入。

实时性

实时性的要求主要体现在广告的及时上下线。如果广告上线不及时,则不能及时产生收入;反之,如果广告下线不及时,则会引发广告超投,也是一种收入损失。

时序性

时序性比较表现在,同一广告的上线、修改、下线等改动,作为消息传递给广告引擎时,必须保证时序性。不然广告的上线和下线等投放状态会出现错误。

二八原则规律的一些特征

  • 80%的收入来自20%的大客户
  • 80%的收益得益于20%的数据

广告收入异常原因追踪总结

本文内容梗概

总结和列举作者常见的广告收入异常的原因,总结问题定位和修复的经验。

因为作者日常多从事广告引擎的开发工作,所以下面的内容往往从广告引擎的视角来阐述。

本文内容多依赖数据监控的一些指标,比如流量、广告展示数、广告点击数、CTR、CPC、CPM、广告召回率、广告存量等。

正常的收入曲线特征

以下特征,仅适用于pv、广告数量级大,收入高的广告系统。

  • 收入曲线平滑变化,没有骤升骤降。
  • 收入变化呈现周期规律特征
  • 同比和环比没有大的变化。日期上与近几日或上周同一时间、上月同一时间,时间上取相同时间间隔内(5分钟)的收入相比,没有特别大的变化
  • 曲线变化符合当日特殊事件给广告收入带来的影响,比如双十一春节等节日、国家重要事件发布、大面积自然灾害等。

如果不满足上面的特征,则收入可能异常。

广告收入异常的数据表现

  • 流量异常
  • 广告展示量异常
  • 广告点击量异常
  • CTR异常
  • CPC、CPM异常

不同原因数据指标的表现不同

  • 流量异常

流量的异常,往往也导致展示量、点击量降低和升高,但是CTR、CPC、CPM等指标不会发生变化。这种情况将收入下降的排查范围缩小到流量端。

  • 广告展示量异常,但流量没有变化
  1. 如果广告召回率(广告存量)是否有异常,排查范围则缩小至广告上下线流程。比如系统问题导致广告上下线异常,广告主是否有大量的上下线操作等。
  2. 如果广告投放机有问题,则把排查范围缩小至广告投放机。
  • ctr 异常
  1. 广告展示量、CPC、CPM不变,则广告主投放的广告质量降低,广告创意素材吸引力差等。
  2. 广告展示量不变,CPC、CPM异常,有可能是由广告主调整价格。
  • CPC、CPM异常,流量、ctr、展示量、点击量都无变化
  1. 算法和竞价策略
  2. 广告主调价

为便于浏览,可参考下面的鱼骨图:

广告收入异常原因

总结

  • 有一个直观易读的数据监控和展示平台非常重要,有利于排查问题的效率。
  • 问题的排查特别依赖数据监控平台,因此数据监控平台必须先保证高可用和数据准确。

技术随笔

此文内容为未经整理的技术工程碎片。

分布式技术已经非常普及,但是为了提高系统的qps和并发能力,我们为什么优先挖掘机器潜力(提高硬件利用率),而不是优先扩容集群。

原因:

  • 服务器运维费用比电费贵多了。提高硬件利用率,虽然不节省电费,但是省服务器。

架构思想

  • 分层 (垂直分离)
  • 分而治之 (水平分离)
  • 动静分离、冷热分离、读写分离
  • 缓冲区
  • 扩展和复用
  • 总线思想、归一
  • 保持简单

方法论

  • 代码和配置分离

工程师手记-将多进程后台服务改造为多线程

背景

2016年秋,部门计划将移动广告引擎和新的移动DSP引擎做架构融合。保留原来的移动广告引擎的业务逻辑,将其移植到新的框架当中去。

新框架有很多特点,其中之一是所有模块都使用了多线程模型而老的移动广告引擎的一个模块则使用了多进程模型。

改造注意点

  • 临界资源的共享
  • 单例资源
  • 内存的共享

改造的陷阱

改造方法

在多进程模型中,单例模式可以安全地被使用。但是在多线程环境中,则要考虑多线程都要抢占单例类,单例类会成为瓶颈,而且还有可能出现线程不安全的问题。

解决方法:

将多进程的单例类,改造成进程体里多例模式,但是在每个线程体内单例。具体方法参考线程安全的单例模式

改造结果

改造成功,并且顺利上线,正常服务。

改造后带来的好处和坏处

  • 性能的提升
  • 内存

广告引擎技术总结

日志尚未完成、持续总结中……

广告引擎,是适用于广告搜索场景下的搜索引擎。

广告引擎特点

广告引擎特点,也是由广告业务需求所决定的。

  • 流量大
  • 低延迟
  • 生产的数据量大
  • 高可用
  • 实时性

详细阐述,请看这里

业务

  • 业务的抽象

架构

  • 广告检索流程
  • 前端
  • 索引
  • 实时数据流

详细阐述,请看这里

算法

  • 竞价排名
  • 机器学习

trouble shoot

性能测试

  • 压力测试
  • IO测试

监控

  • 系统监控

CPU负载、内存使用率、qps、连接数、网络通信延时、第三方服务网络通信延时、计算延时、分业务分模块统计延时、网络IO。

  • 业务监控

广告召回率、广告返回为空比率、ctr、CPC、CPM、展示量、点击量、索引广告数目。

运维

测试

团队

  • 流程管理

Linux Shell 命令小技巧

Top 监控某一多进程任务

以nginx 为例: d PL=pidof nginx

PL=${PL//\ /,}

top -p $PL

SSH

ssh scp等消除每次问yes/no

ssh -o StrictHostKeyChecking no dev.zuocheng.net

grep

常用参数 -Irn

find

清空所有log文件

find ./ -name *.log | xargs truncate -s 0

删除所有nohup文件

find ./ -name nohup.out | xargs rm -rf

find ./ -name *.swp | xargs rm -rf

删除过期日志(60天前)

find ./ -mtime +60 -name log.* -exec rm -rf {} \;

把当前目录下所有文件都MD5

find ./ -type f -exec md5sum {} \; | sort -k 2 > md5sum

date

依据时间戳获取时间

date -d @1525856172

生成日期
DATE=$(date +%Y%m%d)

比较两个文件夹的不同

生成MD5值 find ./dir -type f -exec md5sum {} \; > md5sum.txt

生成MD5值,并排除日志文件 find "dir" -path 'dir/log*' -prune -o -type f -exec md5sum {} \; > md5sum.txt

校验md5值 md5sum -c > md5sum.txt

使用Thrift的网络框架搭建一般性网络应用

Idea的提出

Thrift 存在的一些问题:

  • 相比于protobuf,Thrift的序列化和反序列化性能表现欠佳,大概比protobuf慢10倍。
  • 相比于其他RPC框架,Thrift拥有优秀的底层通信框架。(作者简单比较过thrift和grpc1.0的通信框架,grpc的设计实在太过简单。)

由此提出猜想和假设:

  • 将 Thrift 的底层通信框架抛离出Thrift框架,利用其来构建一般性的网络应用。
  • 组合 Thrift 的底层通信框架 和 protobuf序列化协议,使之成为一个新的RPC框架。

从实现难度和工作量上的考虑,本文尝试实现第一个假设,“将 Thrift 的底层通信框架抛离出Thrift框架,利用其来构建一般性的网络应用”。第二个假设希望日后,作者在时间和精力富余的时候再进行试验。

使用Thrift的网络框架搭建一般性网络应用的优点

  • 快速搭建网络应用,节省时间成本
  • 当Thrift协议序列化和反序列化成为系统性能瓶颈时,可对其进行替换,同时又能保留Thrift的网络框架,减少对上下游系统的影响

如何操作

有两种方法:

  • 在IDL文本中,将自定义协议的结构体存为一个thrift的string变量。
  • 构建自定义的Processor类

下面对这两种方法做详细介绍:

在IDL文本中,将自定义协议的结构体存为一个thrift的string变量

举例:

namespace cpp com.thrift.test

struct Parameter{
    1: required string bin_data;
}

service DemoService{
    i32 demoMethod(1:string param1, 2:Parameter param2);
}

将新的协议序列化后的数据放入bin_data中,这种方法缺点是,自己定义的协议,还要被thrift的序列化反序列协议包裹,不能完全消除thrift序列化和反序列化的代价。

第一种方法太过简单和粗糙,因此经过挖掘thrift代码后,探索出了更精细的方法。

构建自定义的Processor类

Thrift 底层通信模块的四大基类,TServer、TProcotol、TProcessor、TTransport,其中TProcessor::process是负责处理具体业务逻辑入口。

class TProcessor {
 public:
  virtual ~TProcessor() {}

  virtual bool process(boost::shared_ptr<protocol::TProtocol> in, 
                       boost::shared_ptr<protocol::TProtocol> out) = 0;

  bool process(boost::shared_ptr<apache::thrift::protocol::TProtocol> io) {
    return process(io, io);
  }

 protected:
  TProcessor() {}
};

因此,只要自定义实现TProcessor的基类,重写process方法,就能自定义自己的网络应用。

下面是一个Hello world应用的简单实现:

首先实现一个HelloWorldProcessor 类。'

class HelloWordProcessor : public apache::thrift::TProcessor {
public:
  virtual bool process(boost::shared_ptr<apache::thrift::protocol::tprotocol> in, boost::shared_ptr</apache::thrift::protocol::tprotocol><apache::thrift::protocol::tprotocol> out) {
    out->writeBinary("Hello world!");
    out->getTransport()->flush();
    out->getTransport()->close();
    GlobalOutput.printf("send bytes %s", "Hello world!");
    return true;
  }
};

然后构建main函数,本实例使用TSimpleServer模型

using namespace std;
using namespace apache::thrift;
using namespace apache::thrift::processor;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;
using namespace apache::thrift::server;
int main(int argc, char **argv) {
boost::shared_ptr<tprotocolfactory> protocolFactory(new TBinaryProtocolFactory());
boost::shared_ptr<tprocessor> processor(new UwsgiProcessor());
boost::shared_ptr<tservertransport> serverTransport(new TServerSocket(9090));
boost::shared_ptr<ttransportfactory> transportFactory(new TBufferedTransportFactory());
TSimpleServer server(processor,
  serverTransport,
  transportFactory,
  protocolFactory);
printf("Starting the server...\n");
server.serve();
printf("done.\n");
return 0;
}

最后编译、链接和运行。

简单实现一个socket客户端,发送请求,就能得到HelloWord。

性能测试

待完善

Thrift 底层通信框架的优化和调优

待完善

本文小结

作者写本文,正是在工作中遇到了一些真实的问题,比如thrift序列化反序列化慢,thrift底层那么优秀的通信框架如何更高的加以利用呢?因此带着工作中的一些问题,开始阅读thrift的源代码。

除了本文中的一些实例,作者还做了一个小的代码库,里面就用到了本文中的方法,单独使用了thrift了网络框架,Github地址如下:https://github.com/zuocheng-liu/GI

HTTPS技术简介 – 团队分享

背景

苹果曾经发布新闻,到2017年1月1日为止,IOS上所有app只能支持https链接。因此几乎所有在IOS有app和业务的公司,都赶在截止时间升级自己的app和后端系统。

我们公司的广告系统也不例外,也需要升级。正巧2016年12月29号的团队每周技术分享,轮到了我,于是把HTTPS的相关知识简单整理了一下,分享了出来。

PPT 地址如下, 可直接进行下载,无需积分。仓促之作,粗陋难免,哪怕能帮助到1个人,作者心里也是高兴的。

http://www.docin.com/p-1827559135.html

分享思路

  • 团队中大多数人对HTTPS肯定都有所了解,只是可能对HTTPS的技术细节不是很清楚。
  • 分享最开始,先抛出几个问题,吊起大家的好奇心。
  • 其次分享HTTPS安全的原因是什么?如何使用?性能如何?
  • 最后再分享HTTPS的原理,把最难学习的东西放到最后面。
  • 增多听众感性的认识,比如实际抓包演示ssl链接的建立过程。
  • HTTPS 的加密涉及到密码学的内容,尤其是中间人攻击的部分,比较烧脑。又因为分享的时间有限,只能简单提及,不能详细讲解。

分享大纲

  • 抛出几个问题,比如为什么https安全?
  • HTTPS 简介
  • HTTPS 的优点和缺点
  • HTTPS 性能
  • HTTPS 小型网站部署实践
  • HTTPS 大型网站部署实践
  • HTTPS 协议和原理 -- TLS 单向认证、双向认证 -- TLS 链接建立、加解密过程
  • HTTPS 相关资源分享

分享的具体内容

具体内容在PPT中。

个人对HTTPS的几点认识

HTTPS会成为以后Web传输协议的主流吗?

个人认为会的,尤其是看到苹果对https热衷,https非常可能会成为web传输协议的主流。

我们自己为什么不颁布CA证书

  • 理论上可以,但是自己颁布的证书,被浏览器视为不信任的。而现有CA证书颁布机构都被现在主流浏览器视为信任。

HTTP 升级 HTTPS 会遇到哪些问题

  • 要支持HTTPS,就要全站部署,在HTTPS链接中,任何对http资源的引用都无效,比如图片、css、js等,所以要求引用的资源也要使用https链接。
  • 待补充

结合自己亲身项目,从HTTP升级到HTTPS的过程和方法?

自己负责的项目是广告投放系统,结合自己的经历,总结以下几点注意的地方。

注意时间点

苹果曾宣布2017年1月1号为截止时间。虽然之后宣布延期,但是很多app,已经发布只支持https的新版本。

对广告投放系统来说也要提前升级。

链接的替换

  • 只能在IOS上投放的广告资源中使用https,android不需要。因为运行在android比较旧版本sdk上的app,并不支持https。
  • 链接的替换有两种情况: 1, 直接将http:// 转换为https:// ;2,对于直接转换无效的url,则需要域名映射后进行再进行转换。

后端服务器对HTTPS的支持

  • 广告投放后的打点请求,或者落地页请求,需要回到后端Server,这些后端Server也要支持https。
  • 后端不同服务之间交互可以不使用https链接。

工程师手记-升级PNI以支持PHP7

本文内容简介

  • 简要介绍本博客写作背景和目的:升级PHP Native Interface的代码使其支持PHP7。
  • PNI 升级以支持PHP7的过程做简单介绍。
  • 对这次升级的思路和方法进行总结。
  • 思维发散,假设其他情况下应该用什么样的方法进行升级php7的扩展。

背景和目的

PNI在2016年10月之前仅支持php5系列的版本。9月份时进行升级,使其支持PHP7。

PNI是什么? 具体参考这边博文《PHP Native Interface》。PNI代码规模只有1000行左右,升级大概花费了一周时间(工作外时间),其中包含2天寻找思路、2天的代码升级,3-4天的问题排查。

这篇博客记录了一下PNI升级过程,包括自己学习PHP7扩展框架、API定义的过程,调试、压测、修复bug的过程等。

把这些东西记录下来,主要是为了总结出自己的学习方法和操作方法。写成博客后,可以更好地帮助自己反思升级过程中是否有哪些不好的地方。当然其中也会有自己的成功经验。

PNI 升级过程简述

面临哪些问题,以及有哪些不确定的地方?

  • 代码从何处改起?
  • 如何保证编译通过?
  • 功能能否保证不变,以及PNI自身定义的PHP接口是否需要发生变化。
  • 升级过程中遇到bug和陷阱怎么办?
  • 这次升级的代价有多大?

升级PNI,从哪入手?寻找突破点的过程。

用搜索引擎搜到了几篇博客,都是介绍PHP7和PHP5的 zend api 不同的网文。随意浏览了一下,感觉帮助不是特别大。

自己下载了PHP7.0的源代码,编译一下,稍微看了一下 Zend的源码(主要看Zend_API、zend_list、zend_hash),少数几个ext里的扩展的源码。

看了后收获特别大, 了解了 php7扩展编写的几个特点:

  • php7 ext 扩展编写框架结构变化小
  • API函数名称变化小,多数无变化
  • API函数参数列表变化大
  • 宏的变化小

升级的思路就有了。

升级方法?

依据发现的php7扩展代码的特点,决定以PHP5版本的PNI的代码为基础,进行升级。代码不需要做太大的变动,更不需要完全重写。

设立3个目标,并按顺序分步骤实现。 1. 升级代码,编译通过。 2. 功能验证通过。 3稳定性验证通过。

为了达到第一个目标,使编译通过,使用下面方法:

  • 参考其他extention,首先对比php7和php5扩展的框架,优先修改PNI的框架。

  • 对比PHP7 源码中的zend_API.h、zend_list.h、zend_hash.h等。

  • 要修改的函数多,可能会有API没有升级被遗忘怎么办? 所以一边升级代码,一边make,看着gcc的错误提示去一点一点改正代码。

达到第2个目标的方法,则多写功能验证的测试case。

达到第3个目标的方法,是做压力测试,观察内存和cpu的使用情况。

  • 观察变量和资源是否被及时回收,是否有内存泄漏
  • 观察cpu负荷是否过高

在升级过程中遇到过最大的一个问题是,pni中遇到了内存泄漏。为了定位内存泄漏的原因,花了非常多的时间。

  • 打日志,定位内存泄漏逻辑。
  • 定位后,添加efree、free等函数调试。
  • 添加efree逻辑无效,打印对象引用计数的个数
  • 找到原因,是存储资源类型的变量,作为另一个对象的属性时,如果对象释放,存储资源类型的成员变量不会主动释放。
  • 解决办法,在对象释放的析构函数中,添加释放资源的逻辑。

总结

总体上说,这次升级PNI比较顺利。

在做的过程中,思路其实并没有上面写的那么清楚,心中也只有个大概思路。

在刚开始时,寻找从何下手的过程中,还是走了一点弯路,但浪费的时间不多。

走的最大的弯路则是,上文说的定位和解决pni内存泄漏的过程中, 没有首先想到去观察对象或变量的引用计数的。这使pni升级的过程直接阻塞了。今后遇到内存泄漏的情况,应该首先观察zval的引用计数数目是否为0.

思维发散

下面开始做各种假设,假设我会面临不同的情况(实际并不存在),我该设计什么样的思路和方法进行PNI的升级呢?

  • 假设PHP7和PHP5扩展开发的框架和结构有大的不同,对PNI的升级,完全重写也许是好方案。

  • PNI的代码规模只有1000行,假设10W行规模以上,我在达到上文说的,第1个编译通过的目标,就不能直接用gcc的错误信息去定位没有升级的代码,不能用一边改代码一边查php7 zend api代码。应该把PHP7的代码都给熟悉了,再去升级代码。

  • 假设升级代码规模大,php7的扩展框架和代码与php5也有大不同呢?学习成本和工程量将变得非常大,这种情况怎么做呢? 1.大规模的代码,逻辑上应该做好分层和抽象。 在升级大规模代码前,应该先写一个小的PHP7扩展,拿来练手,这样学习的曲线就会平缓很多。

git-svn 使用经验

简介

git-svn可以帮助工程师使用git的管理方式,来管理svn代码库。

使用场景:

  • 历史原因,团队代码库使用svn管理,但同时又想使用git强大的分支管理的功能
  • 习惯了git且对svn不了解的工程师,但团队代码只能用svn管理的情况下,可以使用git-svn
  • 代码想从svn逐步迁移至git管理,中间过程中可以尝试使用git-svn

编译安装

安装包、依赖包列表

  • openssl # ssl
  • serf #让svn支持https
  • apr
  • apr-util
  • subversion 1.8+
  • git 1.9+
  • python 2.7+
  • scons 2.3+

其中,git-svn 工具已经被集成在了git 1.9+里,但是其同时也依赖于svn的perl接口。

以下是编译安装步骤和方法,安装路径以/home/liuzuocheng/local/为例:

编译安装apr 1.5+

官网: http://apr.apache.org/

./configure --prefix=/home/liuzuocheng/local/apr

make && make install

编译安装apr-util 1.5+

./configure --prefix=/home/liuzuocheng/local/apr-util --with-apr=/home/liuzuocheng/local/apr

make && make install

编译安装serf

官网: http://serf.apache.org/

scons APR=/home/liuzuocheng/local/apr APU=/home/liuzuocheng/local/apr-util OPENSSL=/usr/bin PREFIX=/home/liuzuocheng/local/serf

scons install

配置 LD_LIBRARY_PATH 写入/home/liuzuocheng/.bashrc

export LD_LIBRARY_PATH=/home/liuzuocheng/local/serf/lib:$LD_LIBRARY_PATH

编译安装subversion 1.8+ / 1.9+

官网: https://subversion.apache.org/

./configure --with-apr-util=/home/liuzuocheng/local/apr-util/ --with-apr=/home/liuzuocheng/local/apr/ --prefix=/home/liuzuocheng/local/subversion --with-serf=/home/liuzuocheng/local/serf

make && make install

同时安装 subversion perl 接口,在subversion源码根目录下执行:

make swig-pl

make install-swig-pl

配置 LD_LIBRARY_PATH 写入/home/liuzuocheng/.bashrc :

export LD_LIBRARY_PATH=/home/liuzuocheng/local/subversion/lib:$LD_LIBRARY_PATH

安装git

官网: https://git-scm.com/downloads

./configure --prefix=/home/liuzuocheng/local/git

make && make install

配置 LD_LIBRARY_PATH 写入/home/liuzuocheng/.bashrc :

export LD_LIBRARY_PATH=/home/liuzuocheng/local/git/lib:$LD_LIBRARY_PATH

安装问题汇总

  • 编译安装serf,执行scons 显示 no module named scons.script

解决方法,配置scons 环境变量:

export SCONS_LIB_DIR=$LOCAL/python/lib/python2.7/site-packages/scons-2.5.0-py2.7.egg/scons-2.5.0/

  • 执行git svn clone 等命令,提示错误 Can’t locate SVN/Core.pm

解决方法,将svn的perl接口软链至git库中:

ln -s /home/liuzuocheng/local/subversion/lib64/perl5/site_perl/5.8.8/x86_64-linux-thread-multi/SVN /home/liuzuocheng/local/git/lib/perl5/site_perl/5.8.8/SVN

ln -s /home/liuzuocheng/local/subversion/lib64/perl5/site_perl/5.8.8/x86_64-linux-thread-multi/auto /home/liuzuocheng/local/git/lib/perl5/site_perl/5.8.8/SVN/auto

使用方法

  • 检出代码 ,类同于 svn checkout 的功能

git svn clone https://svn.zuocheng.net/test/trunk

为了提高下载速度,避免从第一个版本开始下载,可通过添加参数-r$REVNUMBER:HEAD,检出指定版本的代码

git svn clone -r18000:HEAD https://svn.zuocheng.net/test/trunk

  • 从中心服务器的svn repository获取最新更新, 类同于 svn up 功能

git svn rebase

  • 可以在本地建立分支,操作和git相同

git branch master dev

  • 查看提交历史日志 , 类同于 svn log 功能

    git svn log #svn log的展示方式

或者

git log #git log的展示方式

  • 将本地代码同步到Svn服务器

    git-svn dcommit # 将你在本地使用git commit提交到本地代码库的所有更改逐一提交到svn库

  • 解决冲突

1 git-svn rebase , 更新版本,发现冲突文件conflict.c

2 编辑冲突文件vim conflict.c,解决冲突

3 git add conflict.c , 通知git,已经解决冲突

4 git rebase –continue , 重新更新版本,查看是否还有冲突,如果有,跳到第2步,否则继续下一步

5 git-svn dcommit, 提交更改

  • 如何切换远程分支, 类同于 svn relocate 的功能

wiki中曾提到有一个方法,但是在我的环境上不起作用 https://git.wiki.kernel.org/index.php/GitSvnSwitch

  • 其他

官方文档地址: https://git-scm.com/docs/git-svn

使用陷阱

  • 待完善

总结

Web服务压力测试工具BullBench

什么是 BullBench ? (what)

  • 一个可以对web服务进行压力测试的工具
  • 最大特点,BullBench 可以读取 nginx access log 中请求,并将其发送给web服务。
  • 也可以读取自定义文件,使用正则匹配和替换,定制请求,发送给web服务。
  • 可以同时模拟15000并发度请求(具体数值受限于系统配置)
  • 和bullbench类似的软件有 webbench、tcpcopy、jmeter等,与它们相比,bullbench有自己独特的地方,比webbench功能多,比tcpcopy操作简单,比jmeter编写测试用例的学习成本更低。

代码托管地址 : (where)

https://github.com/zuocheng-liu/BullBench

为何编写这个软件? (why)

主要原因是,我曾经在查找一个系统内存泄漏问题时,遇到了一些阻碍:

  • 线下压力测试时,要尽可能模拟线上请求
  • 理想方案是使用tcpcopy,复制线上流量,但是目前没有现成的tcpcopy环境

为了克服这些阻碍,就写一个简单的工具,先是读取nginx access log, 提取请求uri,然后模拟1000个客户端发送给Web后台。

这个工具经过再完善和变得更通用之后,bullbench诞生了。

什么时候用呢? 以及软件的局限有哪些。(when)

使用场景:

  • 压力测试时
  • 追踪bug,复现线上问题

局限:

  • 如果没有请求日志,无法使用bullbench进行压力测试
  • 只能模拟发送HTTP GET请求
  • 不能够处理 HTTP 3XX 重定向服务
  • 其他

作者联系方式 (who):

如何编译? (how )

进入源代码目录,执行make

如何使用?

执行 ./bin/bullbench -h 有详细说明和使用实例

    BoolBench 1.0

    -f <filename> 包含请求的文件,可以是nginx access log,也可以是自定义的数据文件
    -t <num>      请求文件的类型, 1 是 nginx access log, 0 其他 其他, 默认是 1
    -u <url>      请求的url前缀, 不支持 https, 比如 http://www.bullsoft.org
    -H <host>     HTTP请求头字段Host, 默认是 NULL
    -c <num>      并发请求的线程数, 默认是 1000
    -r <regex>    正则表达式,用于提取请求文件中特定的内容. 必须和参数'-t 0' 搭配。
    -o <string>   正则变量拼接后的字符串, 支持 $0-$9
    -h            显示帮助信息
实例1:  ./bullbench -f /var/log/nginx/access.log -u http://127.0.0.1:8080
实例2:  ./bullbench -f /var/log/nginx/access.log -u http://127.0.0.1:8080 -H www.bullsoft.org
实例3:  ./bullbench -f /var/log/nginx/access.log -u http://127.0.0.1:8080 -t 0 -r "[a-z]*([0-9]+)([a-z]*)" -o "/display?a=\$1&b=\$2"

一些参数(how much? how many?)

并发线程数设置多少,依据系统情况而定。

  • 在并发线程数过高的情况下,会打开非常多的连接,常会遇到打开文件数过多的错误
  • 使用root通过ulimit设置 open files的限制,使之变大

STL容器 vector list 性能对比(附带测试方法和测试数据)

最近在重构公司的一个C++模块,逻辑里有排序、过滤等操作。开发过程中,遇到下面的一些问题:

问题和猜想

  • 单链表的归并排序线性表的快速排序的时间复杂度都是 O(nlog(n)),但在实际情况中哪个更快呢?需要测试一下。
  • 猜想的发散,stl vector 和 list(双向链表)的迭代器遍历、顺序插入、clear操作, 时间复杂度好像都是相同的,但谁更快呢?
  • 看stl代码去研究vector和list的实现,并不能得到我们最终想要的结果,测试数据会更直观。

测试总结

先写测试总结,测试方法、测试结果都在下面。

  • 迭代器遍历,list 比vector 稍块,使用for_each 比使用for更快。
  • 顺序插入, vector比list 约快3倍
  • clear操作,vector 几乎不耗时,list 要耗费好多时间,vector比list至少快1千倍以上
  • 排序, vector 大约比list 快2倍。

结合项目,对比vector和list的性能

  • 在我重构前,模块使用的是vector存储数据,排序使用的是快排(大于16小于堆),过滤则是用临时数据vector转存。
  • 在我重构后,使用list存储,排序使用list的归并排序,过滤则是直接使用list的erase操作。
  • 重构前,耗时4000微秒,重构后1500微秒,时间减少60%。
  • list除了删除操作和遍历操作,排序和插入都比vector慢,但是我使用list后,却提升了性能。原因可能有2个: 1.单个元素数据结构大,vector移动这些元素代价较大,而list移动这些元素反而代价很小;2.去掉了中间临时存储,数据的转存代价也比较大。
  • 使用list后,模块的可扩展性也变得更好了。

这次项目最大的收获就是,如果有过滤操作,优选使用list。vector的缺点如下,1.vector快速排序需要移动元素,如果元素占据空间大,移动代价也非常大。2.vector过滤需要借助中间临时存储,直接erase的代价更大。

测试方法

  • 随机生成10万数据量,由vector和list结构的变量分别存储
  • 对vector 和 list 的数据分别做如下, 1)迭代器遍历 2)顺序插入 3) clear操作 4)排序
  • 记录每种操作消耗的时间
  • 多次测试,记录典型的数据结果

测试结果

直接列出测试结果,单位是微秒

vector for_each : 3263
list for_each : 2556
vector traverse : 4783
vector num traverse : 666
list traverse : 3078
vector push_back : 8136
list push_back : 24581
list delete : 7428
vector push_back : 7329
list push_back : 22968
vector clear : 0
list clear : 13079
vector sort : 89512
list sort : 146529

(附)测试程序

Github 托管地址,保持更新:https://github.com/zuocheng-liu/code-samples/blob/master/linux-c/stl/vector_list_performance_test.cpp

#include<stdio.h>
#include<stdlib.h>
#include<sys/time.h>
#include <list>
#include <vector>
#include <iostream>

#define MAX_NUM 100000

using namespace std;

timeval tv;
uint64_t timer;


bool NumCmp(uint32_t a, uint32_t b) { return (a > b); }

void startTimer() {
    gettimeofday(&tv,NULL);
    timer = 1000000 * tv.tv_sec + tv.tv_usec;
}

uint64_t stopTimer() {
    gettimeofday(&tv,NULL);
    timer = 1000000 * tv.tv_sec + tv.tv_usec - timer;
    return timer;
}

int main() {
    vector<uint32_t> v;
    vector<uint32_t> v2;
    list<uint32_t> l;
    list<uint32_t> l2;
    for (int i = 0; i< MAX_NUM; ++i) {
        srand((int)time(0)  * i * i + 1);
        int num = rand();
        v.push_back(num);
        l.push_back(num);
    }

    // compare oper traverse
    startTimer();   
    for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
        *iter;
    }
    cout<<"vector\t traverse\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (int i = 0 ; i < MAX_NUM; ++ i) {
        //v[i];
    }
    cout<<"vector\t num traverse\t :\t"<< stopTimer() << endl;  


    startTimer();   
    for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
    }
    cout<<"list\t traverse\t :\t"<< stopTimer() << endl;  


    // compare oper push_back   
    startTimer();   
    for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
        v2.push_back(*iter);
    }
    cout<<"vector\t push_back\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
        l2.push_back(*iter);
    }
    cout<<"list\t push_back\t :\t"<< stopTimer() << endl;  

    // compare oper delete
    startTimer();
    v2.clear();
    for (vector<uint32_t>::iterator iter = v2.begin(); iter != v2.end(); ++ iter) {
    //    v2.erase(iter);
    }
    //cout<<"vector\t delete\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (list<uint32_t>::iterator iter = l2.begin(); iter != l2.end(); ++ iter) {
        iter = l2.erase(iter);
    }
    cout<<"list\t delete\t :\t"<< stopTimer() << endl;  

    // compare oper push_back   
    startTimer();   
    for (vector<uint32_t>::iterator iter = v.begin(); iter != v.end(); ++ iter) {
        v2.push_back(*iter);
    }
    cout<<"vector\t push_back\t :\t"<< stopTimer() << endl;  

    startTimer();   
    for (list<uint32_t>::iterator iter = l.begin(); iter != l.end(); ++ iter) {
        l2.push_back(*iter);
    }
    cout<<"list\t push_back\t :\t"<< stopTimer() << endl;  


    // compare oper clear
    startTimer();   
    v2.clear();
    cout<<"vector\t clear      \t:\t"<< stopTimer() << endl;  

    startTimer();   
    l2.clear();
    cout<<"list\t clear     \t :\t"<< stopTimer() << endl;  

    // compare oper sort
    startTimer();   
    std::sort(v.begin(), v.end(), NumCmp);
    cout<<"vector\t sort    \t :\t"<< stopTimer() << endl;  

    startTimer();   
    l.sort(NumCmp);
    cout<<"list\t sort    \t :\t"<< stopTimer() << endl;  

    return 0;
}

C/C++ 中如何写“空语句”

最近我的同事和一些网友都说C/C++中“空语句”(就是单独一个分号的语句)具有延时的作用,可以用来写延时代码。其实这是一种错误的理解。

首先,有人认为空语句经编译后,生成汇编代码是“NOP”指令,NOP指令是空操作指令,执行一个指令周期时间,所以认为C/C++中的“空语句”还有延时的功能,其实这是错误的,“空语句”是不会生成任何有效的指令代码的,是不具有延时做用的。

有人说如下代码是具有延时做用,实际上下边的延时功能主要是加法运算和条件判断运算指令起到了延时的作用。

define DELAY asm("nop");

Google C++ 代码风格示例

They say a good example is worth 100 pages of API documentation, a million directives, or a thousand words.

最近项目中决定使用Google的C++代码规范,然后写了两个代码示例文件(.h 和 .cc),这两个文件中的代码都是用Google的C++代码规范。

写示例代码的好处

  • 减少查看文档时间,降低学习成本
  • 帮助程序员快速了解Google的代码风格的概貌
  • 用于参考和模仿,帮助快速上手

代码托管地址

下面地址保持持续更新:

https://github.com/zuocheng-liu/code-samples/tree/master/code_style

仓促之作,错误和疏忽不可避免,但保证后续不断更新。

初版代码示例

建议点击上面的链接,获取最新版。编辑器的原因,缩进的显示可能有问题。

// Copyright (c) 2016, Zuocheng.net
//
// Author: Zuocheng Liu
//
// Lisence: GPL
//
// File: code_samples/code_style/google_code_style.h 
// (文件名要全部小写,可以包含下划线(_)或短线(-))
// C++文件以.cc 结尾,头文件以.h 结尾。
//
// Google C++ code Style 代码示例 (简体中文版)
//
// *注释  使用//或/* */,但要统一。
// *注释  每一个文件版权许可及作者信息后,对文件内容进行注释说明
// -------------------------------------------------------------最多不能超过80行
#ifndef CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE_
#define CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE_
// 命名空间的名称是全小写的,其命名基于项目名称和目录结构
// 不要声明命名空间std 下的任何内容
namespace code_samples {
namespace code_style {

// **变量命名**
// 尽可能给出描述性名称,不要节约空间
// 变量名一律小写,单词间以下划线相连
// 少用全用变量,可以以g_与局部变量区分
int g_my_exciting_global_variable;

// 只有在描述数据时用struct ,其他情况都用class
typedef struct CodeStyle {
    uint32_t type;
} CodeStyle;

// **类注释** 
// 每个类的定义要附着描述类的功能和用法的注释
//
// GoogleCodeStyle , 通过代码示例,展现google c++ code style
//
// (按需注明synchronization assumptions,是否线程安全)
class GoogleCodeStyle {
public :

  // ** 声明次序 **
  // - typedefs 和enums;
  // - 常量;
  // - 构造函数;
  // - 析构函数;
  // - 成员函数,含静态成员函数;
  // - 数据成员,含静态数据成员。

  // **类型命名**
  // 每个单词以大写字母开头,不包含下划线
  // 所有类型命名——类、结构体、类型定义(typedef)、枚举——使用相同约定
  typedef enum StyleType {
      // 枚举值应全部大写,单词间以下划线相连
      GOOGLE_CODE_STYLE = 0,
      K_AND_R,
      POCO
  } StyleType;

  // 常量命名,在名称前加k
  const int kDaysInAWeek = 7;   

  GoogleCodeStyle() {}
  ~GoogleCodeStyle() {}
  // **函数注释**
  // 函数声明处注释描述函数功能,定义处描述函数实现.
  // - inputs(输入)及outputs(输出);
  // - 对类成员函数而言:函数调用期间对象是否需要保持引用参数,是否会释放这些参数;
  // - 如果函数分配了空间,需要由调用者释放;
  // - 参数是否可以为NULL;
  // - 是否存在函数使用的性能隐忧(performance implications);
  // - 如果函数是可重入的(re-entrant),其同步前提(synchronization assumptions)
  // 举例 :
  // 初始化函数
  ~Init() {}

  // 普通函数名以大写字母开头,每个单词首字母大写,没有下划线
  // 
  uint32_t MyExcitingMethod(CodeStype &code_style, char *output);
  // 存取函数要与存取的变量名匹配
  inline int num_entries() const { return num_entries_; }
  inline void set_num_entries(int num_entries) { num_entries_ = num_entries; }

  // 类的成员变量以下划线结尾
  int num_completed_connections_;

protected :

private :
  // **类数据成员**
  // 每个类数据成员(也叫实例变量或成员变量)应注释说明用途。
  // 如果变量可以接受NULL 或-1, 等警戒值(sentinel values),须说明之
  int num_entries_;

}; // class GoogleCodeStyle

}} // namespace code_samples::code_style 

#endif //CODE_SAMPLES_CODE_STYLE_GOOGLE_CODE_STYLE

工程师手记-将Memcached内存管理机制移植至Redis

Idea 的提出

  • Redis 有其高效的异步网络框架
  • Memcached 有其高效的内存管理机制

将这两者结合在一起后,会如何呢?开始试验将Memcached内存管理机制移植至Redis。

本篇博客的姊妹篇链接: 《工程师手记-将Redis异步网络框架移植至Memcached》

调研和选型

Redis内存管理的几个缺点:

  • 使用tcmalloc 或者 jmalloc 库,这两个库封装较重,内部特性也较多。
  • tcmalloc 适合小空间分配,稍大的空间分配会有瓶颈。
  • Redis 主要是单线程运行(只在后台任务cache持久化功能处又启动了新线程), tcmalloc 和 jmalloc 有保证线程安全,但对redis来说是不必要的功能。尤其是jmalloc,为线程安全做了很重的设计。

软件选型

  • 并不是把 Memcached 的内存管理直接替换redis的内存分配,而是使用ae-memcached的内存分配方式。
  • ae-memcached 的内存分配和 Memcached在原理上毫无不同,仅是从软件架构上对其进行重构和优化。具体参考:《AE-Memcached 优化记录》
  • 选择 Redis 2.8.24 作为移植受体

Redis代码修改和编译 / 移植方案

  • 从ae-memcached中拿出mem_cache / slab 两个类,直接移植到Redis src 目录中
  • 新建两个文件 mc_malloc.h mc_malloc.c,封装mem_cache,让其提供类似 malloc、 alloc、realloc、free的接口
  • 修改 zmalloc.c zmalloc.h 这两个文件,让其支持mc_malloc
  • 修改 Makefile ,默认MALLOC 使用 mc_malloc
  • 修改bio.c 文件,把zmalloc 和 zfree用 libc的 malloc 和 free 代替,这么做主要考虑到线程安全
  • 编译、运行

代码托管地址

给新的redis起了一个新名字mc-redis,源代码托管于Github上:

https://github.com/zuocheng-liu/mc-redis

性能测试实验

硬件

  • Redis-server 服务端 GenuineIntel 6 Common KVM processor 6 核 2.0GHZ 4G 内存
  • redis-benchmark 和服务端部署在同一台服务器上

测试方法

  • 分别运行原本Redis 和 mc-redis, 分别作为实验和对照,参数为 redis-server --port 7777
  • 启动Redis,运行redis-benchmark 测试三次。重复前面步骤,Redis共重启3次,redis-benchmark共测试9次。
  • mc-redis 的测试也使用上面方法
  • 测试命令 ./redis-benchmark -h 127.0.0.1 -p 7778 -q -d 100
  • 只观察set / get 命令的并发度

测试结果

启动一次redis,做了三组实验,数据如下:

  • mc-redis GET 62972.29 / 58275.06 / 55897.15 (requests per second)
  • redis GET 47281.32 / 62034.74 / 51759.83 (requests per second)
  • mc-redis SET 64808.82 / 59031.88 / 56915.20 (requests per second)
  • redis SET 51733.06 / 53676.86 / 56947.61 (requests per second)

结论

在刚启动时(预热阶段),mc-redis 的 set 和 get 操作,比原版redis 的并发处理能力高大约有 15%-20%。 但是稳定运行后, mc-redis 和 原版redis,性能相差较小。

AE-Memcached 优化记录

优化背景和目的

  • 学习Memcached 代码
  • 将 Memcached 的代码成为自己的技术积累
  • 优化Memcache 代码,提高自己系统分析能力

源代码托管于Github上:

https://github.com/zuocheng-liu/ae-memcached

性能优化

网络模型的优化

  • 网络IO多路复用 + 单线程

  • 将 Redis 异步库 移植至 Memcached

优化动态申请内存机制

  • 使用预分配,减小系统调用 malloc、realloc、free的次数,主要出现在新建/关闭链接时,会有较多的系统调用

部分小的函数使用宏代替

优化Memcache协议命令的解析

  • 调整各个命令的解析顺序,把get 和 set 命令放到最前面

软件架构优化

软件架构优化,保证关键代码性能不变

使用宏加强代码复用

  • 重构verbose日志
  • 重构网络库
  • 重构slab

命令模式重构 Memcache 协议

  • 创建command_service类,统一管理命令的解析、处理

更深层次的抽象

将 stats 、 settings 、 logger 和全局资源进行抽象

解耦

  • 将各个模块接口化,减少模块间耦合,尤其是 slab item memcached之间的耦合
  • 依赖注入原则,增强各个模块的复用,其中mem_cache模块 settings等可以形成框架。
  • logger
  • command service

工程师手记-将Redis异步网络框架移植至Memcached

Idea 的提出

  • Redis 有其高效的异步网络框架
  • Memcached 有其高效的内存管理机制

将这两者结合在一起后,会如何呢?

本篇博客的姊妹篇链接: 《工程师手记-将Memcached内存管理机制移植至Redis》

调研和选型

Memcached 的几个缺点:

  • 使用封装较多的 libevent 异步库
  • Memcached 1.2.2 版本后,开始使用多线程,而多线程上下文切换、互斥锁的竞争带来了一定的性能开销
  • 每次新建tcp 连接都进行系统调用(malloc)申请空间

对Memcached 的一项性能测试

实验验证,多线程不会对Memcached带来性能的提高

参考链接:

对Memcached 1.2.2 的一次基准测试实验

软件选型

  • 从 Redis 3.2.0 (截止此文最新稳定版) 版本中选择 libae、 libanet (ae.h、ae.c、anet.h、anet.c 四个文件)
  • 选择 Memcached 1.2.0 作为移植受体

选取这两个版本的主要原因是,作为Redis 和 memcached 的早期版本,特性较少,代码复杂度低,适合进行初步实验移植。

为什么不直接使用epoll呢? 因为除了异步框架之外,还需要定时器的功能,而redis的异步库中已经有现成的实现。

代码托管地址

给新的memcached起了一个新名字ae-memcached,源代码托管于Github上:

https://github.com/zuocheng-liu/ae-memcached

性能测试实验

硬件

  • Memcached 服务端 GenuineIntel 6 Common KVM processor 6 核 2.0GHZ 4G 内存
  • Memcache 客户端 和服务端相同的另外一台服务器

测试方法

  • 分别运行Memcached 和 ae-memcached, 参数为 memcached -m 1024M -t 6 , 其中原本memcached运行6个线程,而ae-memcached 是单线程
  • 使用memslap 1.0.2, 测试10W次请求,100的并发度的情况下,memcached的处理时间
  • 测试命令 ./memslap --servers=test-server:11211 --concurrency=100 --execute-number=1000 --tcp-nodelay --non-blocking

测试结果

  • ae-memcached 6.709 / 6.878 / 7.362 / 7.196 (seconds) 平均7秒
  • 原版 memcached 5.079 / 5.043 / 5.069 / 5.206 (seconds) 平均5秒多

结论

原版Memcache 处理10W条数据的时间要比 ae-memcached少很多。多线程的确会给Memcached带来性能的提升.我们前面的假想并不正确。

对 Memcached 的持续优化

本文作者不仅只把Redis的异步库移植至Memcached,还对Memcached进行了持续的优化,详细请阅下面链接:

http://it.zuocheng.net/ae-memcached-optimization-zh

对Memcached 1.2.2 的一次基准测试实验

猜想假设

memcached这种系统的瓶颈在网络,不在计算和存储,而使用多线程,只能优化计算。用了多线程,反而会有上下文切换的代价。

测试概要

测试目标

让 Memcached 分别采用 单线程模型 和 多线程模型, 并对其性能进行对比

测试方法

测试软件

实验组 Memcached 1.2.2 (IO 多路复用 + 线程)

对照组 Memcached 1.2.2 (IO 多路复用 + 线程)

Memcached 1.2.2 是 Memcached 众多版本中的一个临界版本,在Memcached 1.2.2 之前的版本采用 “IO 多路复用 + 单线程” 模型, 在Memcached 1.2.2 之后的版本则采用 “IO 多路复用 + 多线程” 模型。

测试工具

memslap 1.0

测试环境

  • 软件 Ubuntu 15.10
  • 硬件 Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz 4核 2G 内存

测试步骤

搭建

  • 编译 memcached

先编译单线程的 memcached 版本。(正常编译,作为实验组)

再编译支持多线程 memcached 的版本(作为对照组), 编译方法如下:

修改configure.ac文件,将下面一行

AC_DEFINE([USE_THREADS],,[Define this if you want to use pthreads])

改为

AC_DEFINE([USE_THREADS],1,[Define this if you want to use pthreads])

执行 sh autogen.sh , 然后执行 ./configure --enable-threads ,最后执行 make

  • 分别运行 实现组和对照组的memcached, 然后对其进行基准测试

命令示例: memcslap --servers=localhost:11211 --concurrency=5000 --execute-number=10 --tcp-nodelay --non-blocking --flush

--concurrency=5000 --execute-number=10 这两个数据多次选型,反复测试。

实验结果

选取典型实验结果数据

  • 实验组 (单线程)

实验组 (单线程)

  • 对照组 (多线程)

enter image description here

实验推导结论

从上面的实验结果看,单线程的性能要比多线程性能略高。

使用Mini PC搭建个人用的NAS/服务器/离线下载

软硬件准备

硬件

  • Mini-ITX机箱
  • Mini-ITX 主板
  • 笔记本内存条 4G
  • Msata 固态硬盘 60G
  • 机械硬盘 500G
  • 拥有公网独立IP的服务器

软件

  • Windows 7
  • Ubuntu Server
  • VirtualBox
  • open-sshd
  • autossh

系统搭建

组装硬件

安装系统

Windows 7

  • 安装windows7是,出现提示框windows安装程序无法将windows配置为在此计算机的硬件上运行

不要关闭对话框,按下shift和f10键。弹出一个cmd,输入cd oobe 回车,再输入msoobe.exe回车,会弹出另一个界面,按照提示进行即可,完成后回到错误弹窗界面单击确定,系统重启。

穿透内网

SSH 反向代理

  • 建立 MiniPC 机器到 Server 的反向代理【MiniPC 机器上操作】

ssh -fCNR 10022:localhost:22 work@server.zuocheng.net

10022 为 server.zuocheng.net 机器上端口,用来与 MiniPC 机器上的22端口绑定。

  • 建立 server.zuocheng.net 机器上的正向代理,用作本地转发。做这一步是因为绑定后的 端口只支持本地访问【server.zuocheng.net 机器上操作】

ssh -fCNL "*:8022:localhost:10022' localhost

20022 为本地转发端口,用以和外网通信,并将数据转发到 10022,实现可以从其他机器访问。其中的*表示接受来自任意机器的访问。

  • 现在在 C 机器上可以通过 B 机器 ssh 到 A 机器

ssh -p 8022 work@server.zuocheng.net

SSH 反向代理经常掉线问题解决

  • Mini Server 上生成RSA公私密钥

ssh-keygen

  • 把公钥推送到 远程主机上

ssh-copy-id -i ~/.ssh/id_rsa.pub work@server.zuocheng.net

  • 创建代理通道,并且自动监控通道链接

autossh -M 20021 -NR 20022:localhost:22 work@server.zuocheng.net

  • 设置为开机启动

/bin/su -c '/usr/bin/autossh -M 20021 -NR 20022:localhost:22 work@server.zuocheng.net' - work 放入 /etc/inid.d/autossh

Git 使用经验总结

常用命令

按使用频度排序

  • git pull
  • git log
  • git diff
  • git commit
  • git commit --amend
  • git review
  • git clone
  • git push
  • git reset --hard/soft
  • git checkout
  • git fetch --all
  • git merge
  • git branch -b develop origin/develop

git 命令非常多,但常用的只有以上几个

常用配置

  • 用户名和密码

$ git config --global user.name liuzuocheng

$ git config --global user.email zuocheng.liu@gmail.com

  • 默认编辑器

$ git config --global core.editor vim

  • 配置比较工具

git config --global merge.tool vimdiff

git config --global diff.tool vimdiff

git config --global difftool.prompt false

git config --global alias.d difftool

  • 或者直接更改配置文件 ~/.gitconfig,添加手工添加配置

常用经验

  • 将远程主干合并到本地分支

在代码上线前,这一步非常重要

git fetch && git rebase origin/master

git fetch --all && git merge origin master

  • 解决版本冲突

暂存本地修改

$ git stash

拉取版本库中最新版本

$ git pull

将本地修改与版本库中最新版本合并

$ git stash pop stash@{0}

解决冲突,使用下面的工具会非常方便

$ git d

  • 将源码导出tar包

git对应的功能是归档

mkdir ../working

git archive master | tar -x -C ../working

Git 高级功能

submodule

git submodule add 仓库地址 路径

git submodule update --init

git submodule update

fork后如何同步源的新更新 ?

  • 首先要先确定一下是否建立了主repo的远程源

git remote -v

  • 如果里面只能看到你自己的两个源(fetch 和 push),那就需要添加主repo的源:

git remote add upstream URL

  • 查看主repo的远程源

git remote -v

  • 拉取主repo源的代码

git fetch upstream

  • 合并

git merge upstream/master

  • 提交

git push

git 项目打包导出

示例, 项目中有为1.0的tag

git archive 1.0 | bzip2 > v1.0.tar.bz2

与 Gerrit 配合使用

提交代码审核 git push origin HEAD:refs/for/mybranch

敏捷软件开发 – 总结

敏捷软件开发简介

  • 一套适应变化,立足于变化的软件开发方法
  • 一套被证明过的目前最有效的软件开发方法

Scrum、 XP 和 敏捷的关系

  • 敏捷是一种指导思想或开发方式(价值观)
  • Scrum和XP是敏捷开发的具体实现方法 (方法论)

敏捷和瀑布

瀑布,主要继承自大工业时代遗留下来的思想,关注流程和契约

敏捷,因互联网兴起一种软件开发思想,应变化而生,关注反馈和沟通

互联网软件开发的特点

  • 需求总是在不断变化

  • 不清楚需求最终是什么,一切需求都来自于未经验证的假设

  • 对软件系统和团队的要求,必须快速适应变化

  • 互联网时代的产品需求来源已经发生变化

  • 传统IT的软件需求来自于客户,软件开发商必须基于合约按期向客户交割软件产品。(契约)

  • 互联网应用软件,以服务提供给用户/客户,需求来源于互联网企业对用户/客户体验或服务自身的挖掘。(变化、不确定性)

互联网开发的问题

  • 什么样规模的团队最能保证效率和安全?

小团队

  • 什么时候才能确定最终的技术方案

编程的时候

软件开发方法的演化过程 (敏捷开发的推导过程)

  • 小作坊式开发

  • 传统瀑布模式(螺旋模式等等一系列模式)

  • 迭代式开发

  • 敏捷开发

Scrum 流程简介

参考维基百科

感性体验

对敏捷的反思

  • A/B 实验

需求是不确定的、易变的,原因之一是因为需求往往来自于假设,对假设的验证可以进行A/B实验。

  • 灰度发布

保证安全,减小风险。

  • 技术驱动

技术驱动能最大的激发团队成员的积极性。

问题

  • 敏捷适合小团队?

并非如此,按照敏捷的思想,团队要保持精简。若原有团队是大团队,则拆分为小团队。

安全的C/C++网络应用的开发流程

本文概要

简要介绍C/C++ 网络应用系统的特点、应用场景,简述适用于C/C++ 网络应用的开发流程。

C/C++ 网络应用系统特点

  • 高性能
  • 高吞吐量
  • 节省内存
  • 开发、测试用时多、开发效率慢
  • 调式成本特别高

C/C++ 网络系统的应用场景

  • 数据接口服务
  • 计算密集型应用,比如搜索引擎、图像处理、统计学习等
  • 特殊领域应用,只能由C/C++ 完成

开发流程的目标

由于上面提到的C/C++ 的特点和应用场景,因此开发流程要达到下面的目标:

  • 安全
  • 控制bug
  • 保证收益

开发流程

需求

因为C/C++ 系统的自身特点,在网络应用中,它们常常担当后端系统、基础服务等。在整个产品的系统架构中,C/C++系统和业务系统常常被分离开来。C/C++ 系统不直接响应来自产品的需求,只响应业务系统提出的技术升级或改造。

设计

  • 日志一定全面,Debug、info、warning、error、fetal
  • 使用参数开关来控制新加的特性,对新的特性一定要追加日志
  • 单例测试覆盖率要尽可能高
  • 对输入的安全检查一定要做到位
  • 可扩展性一定要高

编码

  • C/C++ 代码要遵守代码规范和代码标准
  • C/C++ 代码书写尽量遵从ANSI/ISO 标准
  • 不要使用生僻的语法
  • 代码可读性和可维护性一定要高
  • 一定要书写单例测试

测试

  • A/B 测试

测试环境的搭建

必须多套测试环境

  • 普通测试环境
  • 压力测试环境
  • 沙箱环境
  • 线上小流量环境

必须在每一种测试环境都测试完成后,才能发布到生产环境或在生产环境上推全流量

发布

灰度发布

工程师手记-从PHP工程师到C/C++工程师的转变

本文分享一下,博主从PHP工程师变为C/C++工程师后的一些真实感受。

两句话写在前面

  • 编程语言只是工具
  • 工程师应该用正确的工具干活儿

但从我现在的经历和感受看,两句话真的不靠谱。

简单介绍转变的过程

2012年大学毕业,进入第一家公司,担任PHPer。之后两三年用的编程语言一直是PHP,即使14年中间换过工作,依然写PHP,做的都是公司后台的业务系统。

2015年,语言转为C/C++,负责公司商业广告投放系统检索端的开发。

技术方向的不同

  • PHP 关注业务的实现
  • C/C++ 关注底层

PHP程序员和C/C++程序员日常技术关注点

  • PHP 程序员关注对业务的抽象、关注系统的可扩展性,关注如何设计业务架构,使用什么样的设计模式等等

  • C/C++ 程序员更加关注系统的性能,系统的可伸缩性,关注使用什么样的工具提高性能,关注网络IO模型,数据结构和算法等等。

安全性

  • PHP 程序员关注的安全主要是避免系统漏洞和防止攻击,sql注入、XSS攻击、文件注入攻击等等, 系统安全比较重要。

  • C/C++ 程序员更关注逻辑的安全性, 比如输入输出的边界,异常处理等等,系统稳定最为重要。

PHPer 和 C/C++ 工程师何时相互欣赏?

  • PHPer 看 C/C++, C/C++ 好高级啊,底层实现都知道的那么清楚。我们了解就没有那么深啊。

  • C/C++工程师看PHP, 你们做的那么多应用好高级啊,那么复杂的金融系统,你们怎么就做出来了,如果用C来写,怎么才能写出来呀!

PHPer 和 C/C++ 工程师何时相互鄙视?

  • PHPer 看 C/C++, 你们代码怎么都是面向过程的,可读性那么低,多不好维护,抽象和复用都搞不好,软件架构乱成啥了,软件稍微大些,你们就要拆成服务。

  • C++ 工程师 看 PHP, 你们设计这么多类,代码那么啰嗦,难道不是不过度设计吗?你们写的代码性能能行吗?明明10台服务器就能满足的PV,竟然要200台!

同时有了PHP 和 C/C++ 背景之后,我的工作变成什么样子了?(以下故事基于事实,但与事实略有差异)

  • 老板:“那个项目是PHP的,你来接吧。” 我:“NO”。 老板:“只能你来接啊,我们辛亏有你啊,组里只有你会php啊,您一定要接啊!” 我:“OK。”(幽怨+无奈)

  • 猎头:“我这有PHP职位,薪水丰厚”。 我:“俺转C++了”。 猎头:“C++需求少啊,工作不好找啊”。 我:“俺还要写c++”。 猎头:“看那个PHP职位,背景待遇好”。我:“……”。

  • 某一线公司HR :“你C++履历太短,暂时还不适合我们的职位。” 我:"……"(心里流出两行泪)。HR:"但我们公司有PHP职位啊,你来呀"。我:“俺不去了……”(秋风吹皱偶滴心)。

博客写完了, 现在感觉,开头那两句话,说的无比正确。

开源消息队列Kestrel的安装、部署和运行

本文测试用的kestrel版本是2.4.1

下载

  • 下载官网已编译的包

    wget http://robey.github.com/kestrel/download/kestrel-2.4.1.zip
    unzip kestrel-2.4.1.zip

  • 下载源代码自行编译

环境配置

Java 的运行环境配置为1.60版本,以保持兼容

修改配置文件

修改 config/development.scala 和 config/production.scala,一个是开发环境,一个生产环境的配置

监听的IP和端口

队列持久化路径

queuePath = "/home/liuzuocheng/var/spool/kestrel"

日志目录

loggers = new LoggerConfig { 
    level = Level.INFO handlers = new FileHandlerConfig { 
        filename = "/home/liuzuocheng/var/log/kestrel/kestrel.log" roll = Policy.Never 
    }  
}

修改启动命令

生产环境启动脚本scripts/kestrel,将APP_HOME目录修改为实际目录地址

APP_HOME="/home/liuzuocheng/local/kestrel"

运行kestrel

调试环境启动脚本

sh script/devel.sh

生产环境后台启动kestrel服务,使用如下命令

nohup .script/devel.sh &

Rest – 架构风格与基于网络的软件架构设计 – 总结

简介

本文内容是博主的一篇读书笔记。读的书是Roy Thomas Fielding的博士论文 Architectrual Styles and the Design of Network-based Software Architectures (《架构风格与基于网络的软件架构设计》)。

为什么想读这篇论文?主要因为REST架构风格和Restful的框架,在Web系统设计中越来越流行。 Rest这个词,在软件研发人员之间,尤其是博主周围同事、同学之间,不断被提起,可见其影响之大。学习追踪了一下Rest,知道Rest架构风格最早由Fielding在他的博士论文中提出,于是就开始读这篇论文。

本博客可以成为一篇论文的导读,想阅读但未读 Fielding博士这篇论文的读者,希望本博客能给您帮助。

阅读此论文前的一点准备和对论文的一点小总结

Fielding博士的论文写成的时间在2000年,猜测Rest的概念形成时间比2000年还早。

而本博客写于2015年末,与论文写成已有十几年间隔,互联网领域里,无论是行业还是技术,发展太快,好多东西估计也让Fielding难以预料。2000年时,有很多概念并不明朗,好多软件还没有出现,因此论文中有一些东西,让现代的人看很具体,但论文中却写的并不清楚。

读Fielding博士的论文最好立足于2000年的环境下,从现在(2015年)的角度看论文里的内容,论文里有超前也有滞后的内容,有技术领域里早已被人熟知的概念,也有经典不变的原理和原则。

所以,最好用选择性的并略带批判的观点去看这篇论文,从中找寻具有价值的内容,并鉴别已经过时的内容。

论文目录及各章内容简单总结

第一章 软件架构

软件架构中,被用到的一些概念、术语。

第二章 基于网络应用的架构

话题范围缩小,关注基于网络应用的架构。谈及7个方面的架构属性,可作为评估一种架构优劣的7个维度。

第三章 基于网络的架构风格

话题范围进一步缩小,聚焦基于网络的架构风格。列举了很多种架构风格,并用上一章的提到的7个维度,对它们进行简单评估。

第四章 设计Web架构:问题与洞察力

提出Web系统的架构风格需求,比如需要满足容量大、可伸缩性强。(后面提出Rest风格正是满足了这些需求。)

第五章 表述性状态转移

博主认为,这是这篇论文的核心内容。

主要讲了Rest风格的具体内容,从5个方面约束和规范Web架构。

在描述Rest风格的时候,论文用了一个很特别的例子,这个例子是讲述了一个架构,从无到有,再从简单到复杂演化的过程。过程中,这个架构每一步变化都使用Rest风格来约束它,让架构表现出了很好的可伸缩性。论文中称这是Rest风格的推导过程。

Rest 风格的约束

  • 无状态, 请求的上下文无关
  • 缓存约束,缓存约束于客户端
  • 统一接口,Rest 风格核心特征
  • 分层系统,添加中间层和中间层约束。(类似于现在的Proxy 和 Proxy+缓存,比如nginx、nginx+cache、 msyql proxy、file proxy)
  • 按需代码。按照现在的理解则是,让B/S架构,替换掉以前的C/S架构。

第六章 经验与评估

这一章内容有些杂乱。

论文具有局限性的内容

  • Cookie 违反了Rest (6.3.4.2)
  • 会话状态全部保存在客户端
  • 缓存约束于客户端, 可靠性降低
  • 特别适合分布式超媒体系统

问题总结

  • Web应用程序的无状态性

    http协议作为技术背景的web应用程序请求——应答模式是无状态的

    web应用是有状态的,session、cookies等状态机制+其他辅助的机制(违反Rest)

  • 经常被谈及的Rest风格的URL是Rest定义的吗?

Rest风格是抽象的,而经常被谈及Rest风格的URL,只是遵从Rest风格的一种具体实现,Feilding博士的这篇论文里并没有具体去谈。

  • Rest 风格推荐HTTP中的GET,POST,PUT,DELETE对资源进行访问和控制是吗

此篇论文里并没有具体去写这个问题

后记

博主第一次听人提起 Rest 是 2012年时,当时博主刚大学毕业参加工作,在公司里听前辈工程师们谈起Rest。可惜博主3年后才开始主动去学习有关Rest的内容。

虽然读完了Feilding博士的论文,但是感觉Rest的内容好像不仅有这些,可能是其他人的补充,再去学习下。

2015年12月14日

判断平面上点和不规则多边形位置关系算法

本文列举5中计算点和不规则多边形位置关系的算法。

计算机算法实现方案,最好的是弧长法。

射线法 (铅垂线法、水平线法)

射线法是使用最广泛的算法,这是由于相比较其他算法而言,它不但可以正确使用在凹多边形上,而且不需要考虑精度误差问题。

该算法思想是从点出发向右水平做一条射线,计算该射线与多边形的边的相交点个数,当点不在多边形边上时,如果是奇数,那么点就一定在多边形内部,否则,在外部。

其中铅垂线法和水平线法是射线法的两个具体算法,更容易实现,在坐标计算中更方便。

面积法

面积法的思想是如果点在多边形内部或者边上,那么点与多边形所有边组成的三角形面积和等于多边形面积。多边形的面积公式可以用叉积计算。不过计算面积是会有一定误差的,需要设置精度的误差范围。

点线判断法

对于多边形,如果一个点它的所有边的左边,那么这个点一定在多边形内部。利用叉积正好可以判断点与给定边的关系,即点是在边的左边右边还是边上。

转角法

以被测点O为坐标原点,计算其与所有相邻两个多边形顶点点P[i]之间的角度和,所有角度相加后得到360度,即为点在多边形之内,若得到180度,则在多边形边上,0度则在多边形外。

弧长法

弧长法是改进后的转角法,解决了传统转角法的精度问题。算法思想是,以被测点O为坐标原点,将平面划分为4个象限,对每个多边形顶点P[i],计算其所在的象限,然后顺序访问多边形的各个顶点P[i],分析P[i]和P[i+1],有下列三种情况:

  • P[i+1]在P[i]的下一象限。此时弧长和加π/2;

  • P[i+1]在P[i]的上一象限。此时弧长和减π/2;

  • P[i+1]在Pi的相对象限。利用叉积f=y[i+1]x[i]-x[i+1]y[i]计算Op[i]与Op[i+1]的关系,若f=0,Op[i]与Op[i+1]共线,点在多边形边上;若f<0,Op[i+1]在Op[i]逆时针方向,弧长和减π;若f>0,Op[i+1]在Op[i]顺时针方向,弧长和加π。

由于顶点在原点和坐标轴上时需要特殊处理,为了准确性,应该在每次计算顶点时都用叉积判断P是否在当前边上,另外将π用整数代替可以避免浮点数的精度误差。

Ubuntu 使用问题汇总

SSH 连接Ubuntu反应慢

  • SSH服务默认启用了DNS反向解析的功能
  • /etc/ssh/sshd_config文件中修改或加入UseDNS=noGSSAPIAuthentication no 两行
  • 重启sshd sudo service sshd restart

删除旧内核

uname -a  #使用这个命令可以查看当前系统使用的内核。

dpkg --get-selections|grep linux #列出当前内核,带image的则是已经安装的内核。可以用类似以下命令卸载:

sudo apt-get remove linux-image-2.6.24-11-generic #其中 linux-image-2.6.24-11-generic 为版本号,输全 。

在最新的ubuntu (15.10以上)版本中,执行sudo apt-get autoremove也可以直接卸载旧内核

如何升级 Ubuntu 版本, 比如从 Ubuntu 15.10 升级到 Ubuntu 16.04

终端执行如下命令:

sudo do-release-upgrade -d

如果命令do-release-upgrade不存在,则安装 update-manager-core :

sudo apt-get install update-manager-core

Ubuntu中安装和卸载Gnome、KDE、XFACE桌面环境

  • 安装XFACE: sudo apt-get install xubuntu-desktop
  • 安装KDE:sudo apt-get install kubuntu-desktop
  • 安装Gnome:sudo apt-get install ubuntu-desktop

Ubuntu卸载桌面环境(应备份重要文件)

  • 卸载gnome:sudo apt-get --purge remove liborbit2
  • 卸载kde:sudo apt-get --purge remove kdelibs4c2a libarts1c2a
  • 卸载xface:sudo apt-get --purge remove xfce4

给用户添加sudo权限

  • root权限编辑/etc/sudoers文件

添加一行 xxx ALL=(ALL) ALL

或者添加 xxx ALL=(ALL) NOPASSWD: ALL , 使用不用输密码

配置静态IP后无法解析域名

编辑 /etc/network/interfaces 文件, 添加配置:

dns-nameservers 114.114.114.114 114.114.115.115

update-apt-xapi占用资源过高

  • 后台任务,会定期把cpu和磁盘IO打满
  • 解决方法,直接卸载掉 apt-get autoremove --purge apt-xapian-index

升级Ubuntu版本,比如从15.04升级到16.04

先备份!

sudo apt-get update

sudo apt-get install update-manager-core

sudo do-release-upgrade

Ubuntu 无法重启、关机

此问题发生于Ubuntu 16.04 LTS版本,由内核bug导致。

解决办法是,把内核升级到最新版本。截止本文,Ubuntu 16.04 源中的最新版本是4.10.0-22

sudo apt-get install linux-image-4.10.0-22-lowlatency

sudo apt-get install linux-headers-4.10.0-22-lowlatency

最新版本内核查找方法如下:

sudo apt-cache search linux-image

Ubuntu Desktop 软件推荐

本文所列都是作者正在使用的软件,也都是个人爱好的软件。

适用Ubuntu版本 Ubuntu 15.10 、 Ubuntu 15.04

文本编辑

  • wps-office

输入法

  • 搜狗拼音输入法

    百度上搜索搜狗拼音输入法 for linux 可得到下载链接

浏览器

  • Chrown

    sudo apt-get install libappindicator1 libindicator7 sudo dpkg -i google-chrome-stable_current_amd64.deb sudo apt-get -f install

系统工具

  • SysPeek

    sudo add-apt-repository ppa:nilarimogard/webupd8
    sudo apt-get update
    sudo apt-get install syspeek

下载工具

  • axel 相当于迅雷

sudo apt-get install axel

开发工具

  • vim
  • vpnc
  • git
  • Sublime Text 3

    sudo add-apt-repository ppa:webupd8team/sublime-text-3
    sudo apt-get update
    sudo apt-get install sublime-text

删除原装软件

sudo apt-get remove libreoffice-common
sudo apt-get remove unity-webapps-common  # 删除Amazon的链接
sudo apt-get remove thunderbird totem rhythmbox empathy brasero simple-scan gnome-mahjongg aisleriot gnome-mines cheese transmission-common gnome-orca webbrowser-app gnome-sudoku  landscape-client-ui-install
sudo apt-get remove onboard deja-dup

线程安全的单例模式-以C++代码为例

本文描述3种场景下的单例模式:

  • 进程体内无线程的单例模式
  • 进程体内多线程单例模式
  • 在单个线程体中的单例模式

本文所写单例模式代码都使用懒汉模式。

进程体内单例

注意问题:

  • 如果进程体中运行多个线程,则需要考虑多线程同步访问的问题。
  • 如果进程体重没有运行多个线程,则不需要考虑多线程同步访问。
  • 使用线程同步锁保证多进程同步

使用场景举例 :

  • 日志类、文件读写类
  • 资源管理类

代码示例:

进程体内没有运行多线程的单例模式,无需考虑线程同步与互斥

class Singleton {
  public:
    static Singleton* getInstance() {
        if (NULL == instance) {
          instance = new SingletonInside();
        }
        return instance;
    }
  private:
    SingletonInside(){}
    ~SingletonInside() {}
    static Singleton* instance;
};

Singleton::instance = NULL;    

进程体内运行多线程单例模式,使用系统mutex保证线程安全

class Singleton {
  public:
    static Singleton* getInstance() {
        pthread_once(&g_once_control, InitOnce);
        pthread_mutex_lock(&mutex);  // lock
        if (NULL == instance) {
          instance = new SingletonInside();
        }
        pthread_mutex_unlock(&mutex); // unlock
        return instance;
    }
  private:
    SingletonInside() {

    }
    ~SingletonInside() {
       pthread_mutex_destroy(&mutex);   // destroy lock
    }
    static void InitOnce(void) {
      pthread_mutex_init(&mutex,NULL);  // init lock
    }
    Singleton* instance;
    static pthread_once_t g_once_control;
    static pthread_mutex_t mutex;
};
Singleton::instance = NULL;
pthread_once_t Singleton::g_once_control = PTHREAD_ONCE_INIT;

单个线程体中的单例

某些资源在单个线程体内需要保持单例,即每个线程体内都保持唯一。每个线程体内,对象相互隔离,则无需考虑线程安全问题。

此种单例模式的实例需要调用系统提供的 TLS 接口,放于线程局部存储中。

使用场景举例:

  • 多路复用Socket封装类
  • 网络上下文环境类
  • 线程安全的资源

代码示例

class Singleton {
  public:
    static Singleton* getInstance() {
       pthread_once(&g_once_control, InitOnce);
       Singleton* instance = (Singleton*)pthread_getspecific(g_thread_data_key);

        if (NULL == instance) {
          instance = new SingletonInside();
          pthread_setspecific(g_thread_data_key, (void*)Singleton)
        }
        return instance;
    }
  private:
    SingletonInside() {}
    ~SingletonInside() {
       pthread_key_delete(g_thread_data_key);
    }
    static void InitOnce(void) {
      pthread_key_create(&g_thread_data_key, NULL);
    }

    static pthread_once_t g_once_control;
    static pthread_key_t g_thread_data_key;
};

pthread_once_t Singleton::g_once_control = PTHREAD_ONCE_INIT;
pthread_key_t Singleton::g_thread_data_key;

如果使用 Poco库 TreadLocal ,代码还会简洁很多,性能上也会好很多

    class Singleton {
      public:
        static Singleton* getInstance() {
            if (NULL == instance) {
              instance = new SingletonInside();
            }
            return instance;
        }
      private:
        SingletonInside() {}
        ~SingletonInside() {
           delete instance;
        }
        static Poco::ThreadLocal<Singleton> instance;
    };
Poco::ThreadLocal<singleton> Singleton::instance = NULL;

总结

  • 无论程序是多进程运行还是多线程运行,代码都要尽量兼容线程安全

多线程编程的陷阱

依据《Java 并 发编程实践》/《Java Concurrency in Practice》一书,一个线程安全的 class 应当满足三个条件:

  • 从多个线程访问时,其表现出正确的行为
  • 无论操作系统如何调度这些线程,无论这些线程的执行顺序 如何交织
  • 调用端代码无需额外的同步或其他协调动作

不安全的多线程编程容易造成哪些后果

  • 内存泄露
  • 资源、数据访问冲突、数据不一致
  • 进程空间用尽
  • 计算资源耗尽

* 错误处理不当导致整个进程异常结束

数据访问

  • 冲突
  • 不一致

多线程间共享的标志变量, 可能被编译器优化,存储于cpu寄存器,一个线程将其值改变(内存值),在另一个线程的cpu寄存器对应的值却未同步。

解决方法: 1. 添加 volatile 关键字修饰变量。 并且加互斥锁。 2.不使用共享的标志变量。

内存空间使用的安全性

  • 防止内存泄露,小心使用堆空间

  • 小心使用栈空间

逻辑的安全性

  • 参数检查一定要严格,任何一个不合法的参数能让线程异常,然后整个app异常

  • 容错

内存泄露原因

软件的稳定性

  • 如果一个线程体挂掉

  • 悬空指针

释放时设置为NULL 声明定义时设置为NULL

线程空间限制

  • 所有线程受限于进程的空间
  • 系统会限制线程的栈空间(默认是10M)

在大规模软件设计中,线程数量的设定和栈空间的使用必须要小心。

高效使用栈空间技巧

  • 如果使用大的内存空间,则到堆里去申请
  • 不使用递归算法,而使用循环,也要避免使用递归算法的库
  • 更加细致地拆分逻辑或模块,让函数体量更小
  • 复杂数据类型(类或者结构体)做为函数参数时,尽可能传递它们的指针或引用,以节省栈空间

多线程下的设计模式

资源竞争与互斥

  • 单例模式

循环中不使用wait

多线程下的全局变量、资源

线程安全

语言特性 valatile

内存泄露

和多进程编程相同的陷阱

运维的安全性

如果程序在线程在启动时固定,则在可动态扩展的容器中,可能会有因为线程数过多而不能充分使用cpu资源。在性能压力测试时,常常会引起在cpu资源使用率较低时,系统则已经到达瓶颈。

Nginx 和 PHP 的两种部署方式比较

2种部署方式简介

第一种

  • 前置1台nginx服务器做HTTP反向代理和负载均衡
  • 后面多态服务器部署Nginx Web服务和php-fpm提供的fast cgi服务

第二种

  • 前置1台nginx服务器做Web服务
  • 后面服务器只部署php-fpm服务,供nginx服务器调用
  • 前置1台nginx服务器,在调用后面多例php-fpm服务时,也可以做到负载均衡

如下图 :

 2种部署方式

对比

从系统设计角度

第一种部署是常见部署方式,大中小规模网站都能适用。

第二种,web服务和php-fpm服务部署在不同服务器上,更加细致。但有几个问题:

  • 前置nginx充当Web服务。对静态资源的访问、压缩传输、缓存设置等,也都集中在这台服务器上。一旦访问量变多,压力变大,容易成为瓶颈。
  • 如果静态资源都存放于CDN,不需要HTTP 压缩传输,这种部署方式还算比较合理;
  • 承接上面两点,还可以对这种部署方式进行优化。如前置nginx负载均衡和反向代理,中间是nginx Web服务,后面部署php-fpm服务。

从性能角度

相比第二种部署方式,第一种多走了一次进程间交互。

  • 按照第一种部署,当一个http请求过来,先是nginx反向代理转发至nginx Web服务(通过网络),Web服务再通过fastcgi协议与php-fpm进行交互(进程间交互);
  • 按照第二种部署,当一个http请求过来,充当Web服务的nginx,直接通过网络与php-fpm进行交互

第一种部署,通过网络交互的是HTTP协议,第二种通过网络交互的是fast-cgi协议, 这两种协议对比如何呢?

  • fast cgi 的数据包会比HTTP稍微大一些,fast cgi协议会比HTTP携带更多的参数信息、传输控制信息等等。
  • fast cgi 协议比HTTP协议格式化严格一些,解析起来速度更快一些。

从运维角度

  • 第一种是最常见的部署方式,后面所有服务器上的服务都是同构的,简单粗放。

  • 第二种则是将nginx和php-fpm单独分开部署,不同服务在服务器集群上的分布更加细致。通过统计Web服务中的压力分布,可以更加精细地利用硬件资源。运维成本也更高。

从开发测试角度

两种部署方式都不合适开发环境或测试环境,都仅适用于生产环境。

开发和测试环境把nginx和PHP部署到一台服务器上即可,也不需要反向代理和负载均衡。

总结

如果是LAMP环境的部署,第一种比较常见。

如果不是LAMP,是nginx和其他fastcgi服务交互,比如C/C++、java的fastcgi程序,在大规模的网络应用中,类似第二种的部署是常见的。做到不同服务之间分开部署,反而是简化了系统的网络结构,更加便于维护。

后记

此篇博文的内容,都来自于和百度前同事在QQ群里的讨论。

本博客搭建和定制方法

搭建环境

硬件

  • 阿里云服务器

软件

  • LNMP 环境(Linux + PHP + Nginx + Mysql)
  • WordPress

定制本博客的Wordpress 功能

博客主题

Twenty Eleven, 简约风格

博客文本编辑器

使用了WP-Markdown 插件,可惜不支持GitHub 扩展的特性

HTML h3 标题又小又不明显 怎么办?

在主题目录 wp-content/themes/twentyeleven/style.css 中,h3 元素样式添加 font-weight: bold;更改front-size为12px。

添加缓存,提高博客响应速度

WP Super Cache 插件

让列表页只显示文章标题,而不把文章标题页显示出来

修改主题中文件,在 content.php 中,把调用the_content函数的语句注释掉

在文章末尾添加文章签名,如果文章被别的网站扒去,也可以把来源显示出来。

修改主题中文件,content-single.php,

在content-entry 里添加如下代码

<p style="border:1px dashed white; padding:10px; margin:10px 0;line-height:26px;border-radius: 3px;" >
        <b>转载请注明来源,原地址保持永久更新。</b>
        <br /><b>博客首页:</b><a target=_blank href=//it.zuocheng.net>作程的技术博客</a>
        <br /><b>文章标题:</b><a target=_blank href=<?php the_permalink() ?>>《<?php the_title(); ?>》</a>
        <br /><b>本文链接:</b><a target=_blank href=<?php the_permalink() ?>><?php the_permalink(); ?></a>
</p>

访问 http://it.zuocheng.net/sitemap.xml 提示错误

把xml 注释去掉,修改aioseop_sitemap.php

//$this->comment_string = __( Sitemap %s generated by All in One SEO Pack %s by Michael Torbert of Semper Fi Web Design on %s, 'all_in_one_seo_pack' );

添加链接 (书签、友情链接等)

在数据库中,把wp_options.link_manager_enabled项设置为1

支持HTTPS

使用了 let's encrypt 的证书

所使用的插件列表

  • All In One SEO Pack //用于SEO
  • Disable Google Fonts //禁止调用google字体,国内请求慢,不然会降低网站响应速度
  • Google XML Sitemaps //生成xml,SEO
  • Query Monitor //监控,一般情况下禁用
  • Table of Contents Plus //显示文章结构
  • Wordfence Security
  • WP Clean Up
  • WP Super Cache
  • WP-Markdown
  • WP-PostViews
  • WPtouch Mobile Plugin //让博客对移动端友好
  • 多说 //社会化登陆
  • 百度sitemap //百度搜索SEO
  • WP-Statistics //统计,包括来访、IP、展示数等等

更新、安装 直接下载,不使用FTP

修改wp-config.php,添加命令:

define("FS_METHOD","direct"); 
define("FS_CHMOD_DIR", 0777); 
define("FS_CHMOD_FILE", 0777); 

系统优化

使用 Unix domain socket,减小 LAMP 通信代价

Nginx 和 PHP ,PHP 和 Mysql 通信都使用Unix domain socket,并且把套接字文件放到内存文件系统/dev/shm下面

mysqld 有大量sleep进程,占用过多内存。

添加mysqld配置

[mysqld] wait_timeout=120 interactive_timeout=120

统计过滤机器人列表

服务器迁移

数据迁移

把数据表导出
[admin@src]mysqldump -uroot -password BLOG > /tmp/DB.sql
scp 到目标机 scp /tmp/DB.sql target:/tmp
在目标机导入数据库
mysql>use DB; souce /tmp/DB.sql;
配置权限
grant all privileges on DB to user@'%' identified by 'password';
grant all privileges on DB.* to user@'%' identified by 'password';
flush privileges;

工程师手记-PNI

PNI 简介

PNI,是PHP Native Interface的简写。它是PHP的一个扩展,可以通过它,让PHP直接调用C语言写的函数。

想法源自在百度做项目时,不时地会面临同样一个问题,PHP该如何直接快速地与其他语言交互呢?

PHP 扩展的不足

用C写PHP的扩展是常规的方法。不过使用这种方法总是要面临诸多问题:

  • 作为开发者,要去学习PHP-API、Zend-API,这是不小的学习成本,而且用C做开发测试的效率都不高。
  • 写好了PHP扩展,怎么部署呢,还要找运维工程师去讨论、去争取后才能上线。

来来回回项目进度就被拖慢了。

有如此技术痛点,就找个通用的法子吧。

调研过程

JNI 和 Python

Java 可以通过JNI调用C/C++,Python也有相应的包,比如ctypes

HHVM

稍微去翻了一下HHVM的扩展,发现HHVM有 native interface, 但稍微看了一下,却发现那只是HHVM的native interface,PHP无法使用。

这个项目,模仿JNI,就取名PNI吧

c 对动态链接库的动态调用

c 加载动态链接库及调用函数有现成的方法。

使用dlfcn.h 库中的dlopen、dlsym、dlclose三个函数就够了。

设计和实现过程

接口设计

最开始的方案,就是模仿JNI。因此在最初的实现里,PNI对动态链接库的查询,调用函数的方法都直接模仿了JNI的接口实现。测试使用时,却感觉PNI的接口非常不友好。

和Java、Python 不同,PHP是弱类型语言。JNI 可以在给函数传递参数时,参数的数据类型是已知的,但是PHP传的都是zval,类型并不可知。我把数据类型的控制交给了开发者,在PNI的扩展里,通过struct zval 中 type 字段判断,此种方案让PNI很难于被使用。

于是改进方案,在PNI里添加PNIInteger、PNIDouble、PNIChar和其他几个可以标明参数数据类型的类。

参数压栈

PHP 的函数调用后,动过dlfcn.h库,可以找到函数地址,但是如何调用函数呢?调用函数又如何传参呢?我们无法知道所调用函数的参数列表是什么样子。

由于没有找到比较好的方案,所以就借助于汇编,使用汇编模式C语言的参数压栈方法。

于是写了很多种C语言的函数,主要是参数列表不一样,总结了GCC编译器在x86_64架构CPU下参数压栈的几个特点:

  • 8、16、32位和64位整形的参数,按顺序传到64位的寄存器,当多于6个整形参数时,剩下的整形参数都压栈

  • 32位和64位浮点,都传到64位的浮点计算器,浮点参数多于6个时,剩下的浮点参数都无效

无论C函数的参数是什么样的类型,PNI都将其按64位的整形或浮点处理。

问题解决了

系统兼容性

因为参数压栈的问题,目前PNI只支持GCC编译器和x86_64架构的CPU。其他架构和编译器都没有来得及去实现。

总结

吭哧吭哧,PNI最初版本调试测试成功。

后记

如何让PNI被更多的人知道并使用,怎么做呢?(还在思考中)

  • 开源,传到GitHub
  • 融入社区
  • 在问答网站上多回答相关问题

其他方法呢?再想一下。

工程师手记-赶集易洗车后台和服务端

项目简介

赶集上门洗车业务服务器端,分为两部分:

  • 为洗车APP提供服务的接口
  • 洗车业务相关的管理后台

从整个项目上说,虽是个创业项目,但需求针对性强,从洗车仅一点上切入,易于发力。

相比曾经经历一些创业项目中,产品总是摸不住方向,不知何处发力,试错成本太高,做到最后都迷茫该做什么,怎么做。还有些项目,铺开面去搞,结果摊子太大,顾及不暇。

赶集易洗车与之相比要好太多,对我来说,做这个项目感觉很舒服。负责时间:2014-11 至 2015-08

框架代码

主要开发语言是PHP。典型 LNMP 架构。

代码框架是赶集网多年积累下来的老框架,对于老框架,工程师都是能够理解的,不过还是想说一说。

  • 简易实现的路由分发
  • 没有清晰的分层概念
  • 有一套简单的DB访问工具类
  • 对其他架构系统调用封装访问很全面
  • 赶集所有频道都使用一套框架代码

总之,有优有劣。

业务代码

虽然使用了类,但仍然使用了面向过程的思维在开发。代码复用性、可扩展性比较低。由于业务简单代码耦合还不是很严重。

代码没有分层也是问题。

项目发展过程

框架的改进

赶集易洗车框架和其他MVC框架的诟病一样,表现层逻辑直接调用数据访问逻辑,并没有业务逻辑层。于是劈出一个目录,存放业务逻辑层代码。建立规范约束DB层和表现层,老的业务逻辑代码日后慢慢迁出和重构。

面向对象

老逻辑中类的作用只被用于聚合。多态和继承并没有被使用。

对DB层的再次封装

在DB访问类,由于历史原因,用起来特别不友好,也有让新人不易察觉的逻辑陷阱。

对此,我使用了Service Locator Pattern + Proxy Pattern 这两个设计模式,对原DB层再次进行封装,简化上层对DB层的调用,添加缓存的优化。之所以使用这个方案,主要是根据项目当前情况,有如下考虑:

  • 不能抛弃和重构原有的DB访问类, 因此选择再其之上再进行封装。遵循开闭原则,不对老逻辑进行改动
  • 原有DB访问类有逻辑陷阱,因此使用Proxy Pattern,把陷阱的规避放到proxy类中,优化对外的接口
  • 原有DB类,每一个数据表都对应一个DB类,全局只需初始化一次,但会在不同的地方多次调用,所以选用Service Locator Pattern,每个DB类,初始化1次便缓存起来,供之后逻辑重复使用

用了上面的方案后,代码可维护性和可读性都提高了。

展示列表的封装

后台有很多的展示列表页面,主键往往是一样的,比如都是订单的ID,但是其他的列,有的一样,有的不一样。可以看成,不同的列表页面,就是不同的列的组合。

使用Decorator Pattern 对其进行重构。这个设计模式比较简单,但是实在是特别好。在我之后的开发工作中,凡是遇到列表,开发的时间估计就能缩小一半。维护的成本则是更低。

具体业务开发

具体业务的开发,也许是最枯燥的工作,不具有挑战性,只能付出劳力。估计只有在机器人程序猿出现后才会解决这个问题。

关于团队

团队每周有分享,可怜创业项目排期紧张,团队分享没有坚持几期,后来慢慢停掉。原因我认为可以从两个方面看:

  • 随着业务压力的增大,团队给予技术成长上的提升关注也会消弱。
  • 人有惰性,团队往往也是如此。

我在团队分享3次,《地理区域和点关系计算》、《代理和反向代理》、《PNI》 , 分享积分是团队最高的,本来有一部kiddle做奖品,团队解散后也没去要过来,哈哈。

开始写工程师手记

从12年4月大学毕业开始实习,至今工作3年半经验。

回忆这三年半,这些经验都给我带来了什么呢?不容置疑,有很多东西,但是具体有哪些呢?不好好收拾一下脑海,真就不能立刻说出来。

所以昨晚上班路上就想出了写工程手记这么一个idae,有了手记,我就不会对做过的项目有种“彻底忘记”的感觉了。自己做过什么,一看手记立刻就能回忆起来,曾经的经验、走过的陷阱、当时的思考,都可以立刻回到自己的身上。

通过工程手记,还可以记录在做项目过程中的所感和所想,以后也可以拿来回顾和反思,说不定能得到更多的东西。

总之,就是让自己以往的经历,不仅仅只是流逝的时间,只是过往的履历,一定要变成有价值的经验。

手记的内容

  • 项目简介 (what 、 when 、who)
  • 所遇问题 (why)
  • 如何做的呢? (how)
  • 总结和反思

给自己的手记制定几点原则

  • 原公司保密的东西不以泄露
  • 从客观角度描述问题,尽可能不夹带个人感情倾向
  • 不随意否定项目、团队或个人
  • 内容保持精简,不谈无关话题

写手记的想法是怎么来的呢?

主要是源于两件事吧。

刚玩微博时,关注了易胜华律师,从他发的内容里知道他有写律师手记的习惯。他的手记里,都记录着每次办案的一些重要或关键的细节,也包括他思考的过程。看了后深感佩服,不愧是名律师啊。

在大学时,曾是学校越野队队员,队里规定每次比赛完必须要写比赛总结。总结的目的很明确,就是经过每次的比赛,都能让队员有所提高。

所以我们软件研发工程师为什么不能有手记呢?

二进制运算技巧

假设都在x86或x86_64架构CPU上进行运算,二进制基本运算包括,加减乘除、与、或、异或、同或、移位

假设n 为 32 位整形数,取正整数n除以8的余数 : n & 0x07

假设n 为 32 位整形数,取正整数n除以16的余数 : n & 0x0F

假设n 为 32 位整形数,8位对齐 : (n + 7) & 0xFFFFFFF8)

假设n 为 32 位整形数,32位对齐 : (n + 31) & 0xFFFFFF80)

假设n 为 32 位整形数,求8位对齐填充位数 ((n + 7) & 0xFFFFFFF8) - n;

求int 最大值 : (1 << 31) - 1 或者干脆 0x7FFFFFFF

求int 最小值 : 1 << 31

判断奇偶数 : n & 1 == 1

交换a b 两个变量的值 : a ^= b ^= a ^= b;

n 乘以2的m次方 : n << m

spawn-fcgi 源码分析

梗概

本文内容对Spawn-fcgi源码进行解读,简要说明其原理,并具体说明其实现方式。

Spawn-fcgi 源码虽然只有600多行,但是初次阅读起来依然需要花很多时间。为了节省读者的学习成本,提高学习Spawn-fcgi 的效果,作者对Spawn-fcgi的源码做了裁剪,保留最核心的功能和原有的代码结构,且能编译后正常运行。最后代码只有200多行。

源码地址在这里

必备知识

要阅读Spawn-fcgi,读者至少需要掌握以下几个方面的知识或技能:

  • 了解CGI和FastCgi的概念,了解其使用场景
  • 基础的Linux C 环境编程,会使用常见的库函数比如getopt、exec 等
  • 基础的Linux C 的多进程编程,熟悉fork、waitpid、setsid等函数
  • 基础的Linux C 网络编程,熟悉建立tcp连接、select非阻塞方式通信,多路复用I/O等

裁剪后Spawn-fcgi的执行过程

  • 创建服务器socket
  • fork进程,子进程初始化会有两个主要操作:
  1. 把socket的文件描述符,复制到FCGI_LISTENSOCK_FILENO
  2. 会执行execl 函数,运行cgi程序,并让cgi程序拥有子进程的上下文环境

    运行cgi程序后,使用FCGI_LISTENSOCK_FILENO这个描述符,来与webserver进行通信。

如何编译

gcc -o spawn-fcgi spawn-fcgi.c

一行命令即可

如何调用

./spawn-fcgi -f cgi -p 9001 -F 256

裁剪后也仅支持接收这三个参数

作者对于Spawn-fcgi的思考

  • fastcgi 协议规定,fcgi管理器中把网络描述符定为FCGI_LISTENSOCK_FILENO,为了一致CGI程序中复用FCGI_LISTENSOCK_FILENO的套接字,总感觉不是很完美。
  • Spawn-fcgi太简单,不需要单独做一个软件,完全可以集成到cgi程序中
  • Spawn-fcgi使用的是多进程,如果集成到cgi程序中,可以自由选择多进程模型、多线程模型
  • 缺少进程守护监控,spawn-fcgi如果一个进程挂掉,不会被重启。
  • Spawn-fcgi 网络多路复用调用的是select,但现在最常用的是epoll

Spawn-fcgi 补充知识

与原版相比,裁剪后Spawn-fcgi的失去了哪些功能

  • 使用linux套接字文件建立tcp连接功能
  • 对IPv6的支持
  • 对root、group 用户的检查
  • 对windows、Solaris等编译环境的支持
  • 通过进程pid文件获取cgi进程
  • 去除对autoconf等编译工具的依赖
  • ...

不同设计模式的适用场景总结

软件设计过程中,该选择那种设计模式,除了借鉴直接的经验,还可以从以下方面考虑:

  • 领域对象模型中,类之间的关系是什么(继承、实现、关联、依赖、组合、聚合)
  • 容易变化的部分是什么,即易于扩展的部分
  • 不容易变化的部分是什么,即需要复用的部分

按照我们的思路,设计模式之所以多种多样,就是对应的领域模型中上面三个方面有很大不同。把握住以上几点,就能正确地选用设计模式

门面模式,Facade Pattern

  • 门面类和被门面类封装的类之间是继承或者依赖关系
  • 类的内部接口、内部逻辑经常变化;不被需要的对外接口容易变化;
  • 类的对外部接口不容易变

适配器模式, Adapter Pattern

  • 适配器类和被适配的类之间是继承或者依赖关系
  • 需要适配的类,或者其接口容易变化
  • 适配的类的接口不容易变化

原型模式, Adapter Pattern

  • 不同类之间有共同的父类
  • 类的部分属性、方法容易变化
  • 类的另外一部分属性、方法不容易变化,且构建复杂,成本很高

单例模式,Singleton Pattern

  • 类在全局唯一存在,其他类与单例类是
  • 类的唯一性不会变化

多例模式,Multition Pattern

  • 多例类的对象之间是聚合关系
  • 多例类的对象的数量容易变化

策略模式,Strategy Pattern

  • 对象和算法,是依赖关系
  • 逻辑框架不容易变化
  • 策略算法容易变化

代理模式,Proxy Pattern

  • 代理和被代理的类之间是依赖关系
  • 代理者的接口不容易变化
  • 被代理者的接口具体实现容易变化

工厂模式,Factory Methond Pattern

  • 工厂模式产生的对象一般有相同的父类(继承关系)
  • 由工厂产生对象不会变
  • 工厂产生对象所属的类的种类是不断变化(增多或减少)

抽象工厂模式,Abstract Factory Pattern

  • 同工厂模式
  • 变化的是,工厂创建对象所属的类的接口会不断变化

门面模式,Facade Pattern

  • 封装类和被封装的类之间是依赖关系
  • 接口类(封装类)的接口不容易变化
  • 实现类(被封装的类)的接口容易变化

Adapter Pattern

  • 同门面模式
  • 适配器类更倾向于兼容现有系统的接口需求

模版模式,Template Method Pattern

建造者模式,Builder Pattern

  • 构建、配置、注入依赖容易变化,且较复杂

桥梁模式,Bridge Pattern

命令模式,Command Pattern

装饰模式,Decorator Pattern

迭代器模式,Iterator Pattern

组合模式,Composite Pattern

观察者模式,Observer Pattern

  • 观察者和被观察者是关联关系
  • 被观察者的数量或种类容易变化

责任链模式,Chain of Responsibility Pattern

访问者模式,Visitor Pattern

状态模式,State Patter

原型模式,Prototype Pattern

中介者模式,Mediator Pattern

解释器模式,Interpreter Pattern

亨元模式,Flyweight Pattern

备忘录模式,Memento Pattern

软件设计之缓存使用

本文主要讨论分布式环境下,缓存如何在软件设计作用、原理、实现方式及注意问题。

缓存的作用

  • 减小原始数据访问压力
  • 提高资源利用率

缓存的原理

局部性原理

缓存的实现方式

查询算法

  • 散列算法,Hash 、 MD5 等
  • B数、二叉树、有序二分查找等

存储

  • 只将访问量最高的部分数据放入缓存
  • 将数据放到比原始IO速率更高的存储介质中

缓存资源回收

  • RUL 算法
  • 定时清理
  • 设置资源有效时间

缓存的存储介质

  • CPU 寄存器
  • 内存
  • 本地文件
  • 分布式系统(Memcache 、 Redis)
  • 数据库缓存数据表

缓存设计注意的问题

缓存的一致性,Cache coherence

  • 避免数据脏读
  • 多级缓存的一致性协议

系统的鲁棒性

  • 在缓存系统停止服务,但仍能保证整体系统正常运行。因此在使用缓存之前,检查提供缓存系统的有效性。

单机缓存

  • 单机缓存是指,将系统资源存放于每个单台服务器上,而不是集中存储与分布式缓存系统中。 缺陷是,如果原数据发生更改,为保证一致性,则必须调用每一台服务器清理或更新缓存。