之前很好奇为什么printf参数类型跟格式化字符串不匹配的时候为啥有时候可以报错,有时候不能报错。printf的时候如果不匹配经常会看到这种错误:

 format ‘%s’ expects type ‘char *’, but argument 12 has type ‘struct S3ListHead * const’

很多core是因为这种问题导致的,所以能放到编译期检查的话,确实有助于提高代码质量。后来白哥指点gcc有单独的attribute来指定检查匹配。作为一个编译器,真是良心啊。

format (archetype, string-index, first-to-check)
The format attribute specifies that a function takes printf, scanf, strftime or strfmon style arguments which should be type-checked against a format string. For example, the declaration:
          extern int
          my_printf (void *my_object, const char *my_format, ...)
                __attribute__ ((format (printf, 2, 3)));

这样自定义的函数也可以依赖gcc做参数检查了。

详细的参见这里

参考文献:

[1] Declaring Attributes of Functions, https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Function-Attributes.html

介绍一个日志grep的神器。 做系统开发的人都有从海量日志里面定位bug的经历,大家一般用如下几种方法:

  1. head -n xxx|tail -n 1,大概定位位置
  2. 直接grep日志来找到对应的行号并用sed cut一段出来。
  3. 把日志灌倒hive等ETL工具里面。

但是如上几种方式要么太慢,要么太复杂。最近突然想到有没有二分·grep的工具,搜到如下一个工具timcat:

安装:

pip install timecat

使用:

timecat -d '2016-01-02' -s '20:13:14' -e '20:14:13' LOGFILE1.log LOGFILE2.log ...
timecat -s '2016-01-02 20:13:14' -e '2016-01-02 20:14:13' LOGFILE1.log LOGFILE2.log ...
  For more: timecat -h

非常快。

作者的博客讲解如下:

参考文献:

[1] 如何对日志文件进行二分查找?开源文件二分查找工具『timecat』介绍, http://blog.reetsee.com/archives/502

最近在分析一个问题,用python写的测试脚本里面跟store节点通信的时候加了一个starttime和超时,store判断了startime跟当前时间之间是否已经达到超时时间,如果到达就拒绝掉。这个脚本在测试case数少的时候没啥问题,数量大了之后就有超时的问题。开始直接调大了timeout让测试先过去了,最近加了多重timeout判断,不能直接为了测试改参数了,分析一下怀疑timeout只生成了一次,封包代码如下:

def pack_packet(ver, pcode, channel, sess, data):

    fields = (i8(ver), u16(pcode), u16(channel), u64(sess), i64(len(data)), i64(time.time()  * 1000000), u64(0), i64(8000000), u64(0), u64(0))
    header = ''.join(fields)

    crc = u64(crc64(header))
    data_crc = u64(crc64(data))

    return fields + (crc, data, data_crc)

def req_create_replica(replica_id):
    pc = PacketCode
    return pack_packet(0, pc.S3_CREATE_REPLICA, 1, gen_sess_id(), replica_id)

python是对静态对象做过优化,相同的静态对象只维护一个,但是这个看着怎么也不是静态对象。后来打日志发现封包的日志都在日志的最前面,才想起来原来这里:

cases_normal_flow = [

('create secondary replica',
 req_create_replica(pack_replica_id(0x01010101, 'cp3p', 5555555)),
 recv_create_replica,
),

('write chunk with 1 record',
 lambda: req_write_chunk(last_created_replica_id(), REPLICA_HEADER, [
         (pack_record_id(0x11111111, 001, 0x55555555), t_object_id, "abcd"), ]),
 (0, [])
),

('read record written in above step',
 lambda: req_read_record(last_created_replica_id(),
                         pack_record_id(0x11111111, 001, 0x55555555)),
 (0, [pack_lstr("abcd")])
)]

非lambda表达式的部分已经在cases_normal_flow全局对象定义的时候执行过了,所以要么再发送的时候重新修改时间,要么都换成lambda表达式。python的灵活总让人有种错觉觉得怎么写都是对的。这部分如果在c下面,函数指针和函数执行还是泾渭分明的。当然,人生苦短,要用好python。

ccache是加快编译的神器,有了ccache,忘了distcc。但是发现go和c混编的项目里面如果用了cgo的话,go编译的时候ccache会报错。主要是传给ccache的某些参数ccache不认识。错误如下:

/usr/bin/ccache: invalid option -- 'd'
Usage:
    ccache [options]
    ccache compiler [compiler options]
    compiler [compiler options] (via symbolic link)

Options:
    -c, --cleanup delete old files and recalculate size counters
                          (normally not needed as this is done automatically)
    -C, --clear clear the cache completely
    -F, --max-files=N set maximum number of files in cache to N (use 0 for
                          no limit)
    -M, --max-size=SIZE set maximum size of cache to SIZE (use 0 for no
                          limit; available suffixes: G, M and K; default
                          suffix: G)
    -s, --show-stats show statistics summary
    -z, --zero-stats zero statistics counters

    -h, --help print this help text
    -V, --version print version and copyright information

See also <http://ccache.samba.org>.
dpkg-architecture: warning: Couldn't determine gcc system type, falling back to default (native compilation)

之前都是关了ccache,后来想新的版本能不能搞定呢?有人提了Issue但是没看到release上有啥新的fix。直接升了一下版本:

wget https://www.samba.org/ftp/ccache/ccache-3.2.5.tar.bz2
bunzip2 ccache-3.2.5.tar.bz2
tar xvf ccache-3.2.5.tar
cd ccache
./autogen.sh
./configure
make -j 8
yum install asciidoc
make install

然后export set CC=‘gcc ccache’,再重新automake就行了。

但是发现还是一样。后来想可以用shell包一层啊,看网上有相应的方案:

 #!/bin/bash
 ccache gcc "$@"

保存这个到path路径下,改名ccached之类,export set CC=ccached,貌似这样ccache不认识的参数就不传递了?反正是ok了,改天研究下。

分布式环境下经常需要到各个节点启动server,常见的方式推的方式,比如scp到各个结点,但是有时候更新的文件少儿需要scp的文件比较多。这时候可以选择nfs挂载的方式把编译好的文件放到网络磁盘上,然后共享到其他的服务器,这样可以按需使用。

首先配置一下nfs服务器。假设系统都是centos:

yum install -y nfs-utils
yum install -y portmap
rpm -qa | grep nfs

事实上看centos6.5以上portmap应该被rpcbind替代了,而已安装nfs-utils的时候应该顺便安装了rpcbind。之后配置一下需要挂载的磁盘:

文件/etc/exports:
/tmp rz*(rw,async) yf*(ro)

简单解释一下: /tmp是挂载的目录路径,后面跟权限控制,可以是主机名或者ip,rz*表示rz开头的主机,括号里面是权限。整条语句表示把/tmp目录共享以rw权限和async方式共享给rz开头的机器,同时以ro权限共享给yf开头的机器。其他属性可以参考这里:

之后启动nfs服务:

service nfs start

需要挂载机器上同样安装客户端:

yum install -y nfs-utils

挂载到指定的目录:

mkdir fs && mount -t nfs xxx.xxx.xx.xx:/tmp ./fs

xxx指定机器名或者ip

参考文献:

[1] Redhat Linux下NFS的配置及操作, http://www.liusuping.com/ubuntu-linux/Redhat-Linux-NFS-setting.html

在C语言下面我们可能会写出如下的代码:

static pthread_spinlock_t lock;

__attribute__((constructor))
void lock_constructor () {
    if ( pthread_spin_init ( &lock, 0 ) != 0 ) {
        exit ( 1 );
    }
}

int func(xx) {
  int ret = 0;
  if (xx) {
    ret = ERR1;
    goto exit;
  }
  pthread_spin_lock(&lock);
  if (xx) {
    ret = ERR2;
    goto exit;
  }
exit:
  pthread_spin_unlock(&lock);
  return 0;
}

__attribute__((destructor))
void lock_destructor () {
    if ( pthread_spin_destroy ( &lock ) != 0 ) {
        exit ( 3 );
    }
}

这段代码存在下面几个问题:

1. spinlock没有静态初始化函数,需要确保使用前调用了pthread_spin_init.
2. 跳转到exit标记去unlock的时候,并不能保证lock已经被加过锁了。
3. func本身存在潜在的并发问题,一个线程可能跳转到exit去解别的线程加的锁。

根据文档这几种情况下的行为是未定义的。

  The pthread_spin_lock() function shall lock the spin lock referenced by lock. The calling thread shall acquire the lock if it is not held by another thread. Otherwise, the thread shall spin (that is, shall not return from the pthread_spin_lock() call) until the lock becomes available. The results are undefined if the calling thread holds the lock at the time the call is made. The pthread_spin_trylock() function shall lock the spin lock referenced by lock if it is not held by any thread. Otherwise, the function shall fail.

  The results are undefined if any of these functions is called with an uninitialized spin lock.

为了情况1,我们可以考虑通过原子操作实现spin_lock,用一个volatile int64类型的整数来标识当前锁的状态。nginx里面的实现如下:

    /* 
     * Copyright (C) Igor Sysoev 
     * Copyright (C) Nginx, Inc. 
     */  


    #include <ngx_config.h>  
    #include <ngx_core.h>  

    //Function: to achieve spin atomic operation lock method based on ngx_spinlock  
    //Parameter interpretation:   
    //lock: Lock the atomic variable expression  
    //value: Flag, whether the lock is a process  
    //spin: In a multi processor system, when the ngx_spinlock method did not get the lock, the current process in a scheduling kernel in the waiting for the other processors to release the lock time  
    void  
    ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)  
    {  

    #if (NGX_HAVE_ATOMIC_OPS)//Support atomic operations  

        ngx_uint_t  i, n;  

        //Has been in circulation, until the lock is acquired  
        for ( ;; ) {  

            //Lock 0 said no other process holding the lock, then the lock value indicates the current process holding the lock is set to value parameters  
            if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {  
                return;  
            }  

            //If it is a multi processor system  
            if (ngx_ncpu > 1) {  
                for (n = 1; n <spin; n <<= 1) {  
                    //With the increasing number of the actual to wait, inspection interval and lock more  
                    for (i = 0; i <n; i++) {  
                        ngx_cpu_pause();//Tell CPU now in the spin lock wait state  
                    }  

                    //Check the lock is released  
                    if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {  
                        return;  
                    }  
                }  
            }  

            //The current process for the processor, but still in the executable state  
            ngx_sched_yield();  
        }  

    #else  

    #if (NGX_THREADS)  

    #error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !  

    #endif  

    #endif  

    }       

简单解释几个关键点:

  1. ngx_atomic_cmp_set等是nginx封装的原子操作,可以从字面意思理解。能直接对应到gcc支持的一些原子操作。
  2. ngx_cpu_pause是不切换cpu上下文的让cpu让出时间片的操作,可对应到后文的asm(“.byte 0xf3, 0x90”);
  3. ngx_sched_yield 暂时挂起上下文,让cpu调度其他任务。

nginx的实现解决了静态初始化的问题,但是解决不了上述问题2和3。为此我们可以考虑在表征spinlock状态的整形变量中加入线程id,来区分操作者是否是锁持有者。参考实现如下:

typedef volatile int64_t Atomic;

#define _spin_unlock_safe   _unlock_safe

typedef struct {
 union {
  struct {
    int32_t tid;
    int32_t atomic32;
  };
  volatile int64_t atomic;
 };
} CACHE_ALIGNED SpinLock;

static __inline__ int64_t _get_tid() {
  static __thread int64_t tid = -1;
  if _unlikely(tid == -1) {
    tid = (int64_t)(syscall(__NR_gettid));
  }
  return tid;
}

static __inline__ int _try_lock_safe(SpinLock *lock) {
  SpinLock lock_val = };
  return lock->atomic == 0 && _atomic_cmp_set(&lock->atomic, 0, lock_val.atomic);
}

static __inline__ int _unlock_safe(SpinLock *lock) {
  SpinLock lock_val = };
  return _atomic_cmp_set(&lock->atomic, lock_val.atomic, 0);
}

static __inline__ void _spin_lock_safe(SpinLock *lock) {
  int i, n;
  SpinLock lock_val = };
  for (; ;) {
    if (lock->atomic == 0 && _atomic_cmp_set(&lock->atomic, 0, lock_val.atomic)) {
      return;
    }

    for (n = 1; n < 1024; n <<= 1) {

      for (i = 0; i < n; i++) {
        __asm__(".byte 0xf3, 0x90");
      }

      if (lock->atomic == 0 && _atomic_cmp_set(&lock->atomic, 0, lock_val.atomic)) {
        return;
      }
    }

    sched_yield();
  }
}

static __inline__ void _spin_lock(Atomic *lock) {
  int i, n;
  for (; ;) {
    if (*lock == 0 && _atomic_cmp_set(lock, 0, 1)) {
      return;
    }

    for (n = 1; n < 1024; n <<= 1) {

      for (i = 0; i < n; i++) {
        __asm__(".byte 0xf3, 0x90");
      }

      if (*lock == 0 && _atomic_cmp_set(lock, 0, 1)) {
        return;
      }
    }

    sched_yield();
  }
}

简单解释几个问题:

  1. asm(“.byte 0xf3, 0x90”);是intel的一条指令,实际上就是上面的ngx_cpu_pause
  2. sched_yield实现等同于ngx_sched_yield
  3. 用两个int32拼成了一个64位整数,考虑截取了tid有风险,后期可以优化成只用一位表示加锁状态,剩下63位依然给tid用。

参考文献:

[1] The Open Group Base Specifications Issue 6 IEEE Std 1003.1, 2004 Edition, http://pubs.opengroup.org/onlinepubs/009695399/functions/pthread_spin_lock.html

线上server core掉了,看dmesg能看到core的日志,

s3store[16586]: segfault at 2a28000 ip 00007fda20543b58 sp 00007fd9e9894128 error 4 in libc-2.12.so[7fda204ba000+18a000]

但是/proc/sys/kernel/core_pattern指向的位置并没有core文件,改一下core_pattern再跑应该能core出来,不过并不是稳定复现的。所以只能先凭这条日志来分析了。从core的位置看,大概率应该是malloc里面的问题。用

addr2line -e xxx  00007fda20543b58

看到的结果是??:0。怀疑是glibc没有调试信息。看一下系统的glibc版本:

rpm -qa |grep glibc
glibc-common-2.12-1.166.el6_7.7.x86_64
glibc-static-2.12-1.166.el6_7.7.x86_64
glibc-2.12-1.166.el6_7.7.x86_64
glibc-devel-2.12-1.166.el6_7.7.x86_64
glibc-debuginfo-common-2.12-1.166.el6_7.7.x86_64
glibc-headers-2.12-1.166.el6_7.7.x86_64
glibc-debuginfo-2.12-1.166.el6_7.7.x86_64

去centos网站上下对应版本的glibc debuginfo并安装

http://debuginfo.centos.org/6/x86_64/
wget http://debuginfo.centos.org/6/x86_64/glibc-debuginfo-2.12-1.166.el6_7.7.x86_64.rpm
wget http://debuginfo.centos.org/6/x86_64/glibc-debuginfo-common-2.12-1.166.el6_7.7.x86_64.rpm
rpm -ivh glibc-debuginfo-common-2.12-1.166.el6_7.7.x86_64.rpm
rpm -ivh  glibc-debuginfo-2.12-1.166.el6_7.7.x86_64.rpm

之后继续addr2line -e xxx 00007fda20543b58还是没有…

我擦嘞。改天继续。

typedef union {
volatile int64_t atomic;
    struct{
      int32_t pid;
      int32_t atomic32;
    };
} S3Atomic;

int main(int argc, const char *argv[]) {
    S3Atomic atomic = ;
    printf("%x\n", atomic.pid);
    printf("%x\n", atomic.atomic32);
    printf("%lx\n", atomic.atomic);
    return 0;
}

参考如下代码

/* 
 * This sample shows definition and initiation of a struct and a union in a struct.
 * using GCC to compile this C file
 * Author: Jay Ren 
*/

#include <stdio.h>

int main(int argc, char *argv[]) {
    struct my_struct1 {
        int num1;
        union {
            int num2;
            char ch;
        };
    };

    struct my_struct1 my_st1 = {
        .num1 = 111,
        /* the following commented line will cause a syntax error. */
        /* .num2 = 123456,*/
    };

    /* num2 or ch in the union of the struct can't be initiated before. */
    my_st1.num2 = 123456;

    printf("my_st1.num1 = %d\n", my_st1.num1);
    printf("my_st1.num2 = %d\n", my_st1.num2);

    struct my_struct2 {
        int num1;
        union my_union {
            int num2;
            char ch;
        } my_u;
    };

    struct my_struct2 my_st2 = {
        .num1 = 222,
        /*  the following line for initiating num2 works fine. */
        .my_u.num2 = 123456,
    };

    printf("my_st2.num1 = %d\n", my_st2.num1);
    printf("my_st2.num2 = %d\n", my_st2.my_u.num2);

    return 0;
}

参考文献:

[1] 联合体(UNION)在结构体(STRUCT)中的初始化(GCC语法), http://smilejay.com/2011/12/gcc_union_in_struct/

线上运维过程中切日志用了logrotate,但无奈日志打的太多,一天的日志几十G难以分析,遂决定改成每小时切分一次日志。从logrotate的说明看有hourly的支持,但是直接把daily改成hourly之后启动报错:

unknown option 'hourly'

感觉是logrotate 3.8.7的版本不支持hourly语法

rpm安装logrotate高版本的包提示缺少fillup和其他的依赖,同时glibc的版本也要求高版本。于是下载了logrotate的源码安装,以最新版本3.9.2为例

./autogen.sh
./configure

提示缺少libpopt头文件,下载了libpopt 1.5的源码安装,提示libtool版本不对。我擦嘞。

后来突然想到libpopt是debian下的命名,试一下centos下

yum install popt-devel -y

搞定。于是继续源码安装,直接用logrotate的官网版本好了。

yum install popt-devel -y
wget https://fedorahosted.org/releases/l/o/logrotate/logrotate-3.8.6.tar.gz
tar zxvf  logrotate-3.8.6.tar.gz
cd logrotate-3.8.6 && make && make install

这个版本是可以稳定使用的。中间试了几个别的版本,3.8.3还是不支持hourly语法,3.8.5支持了语法,但是测试的时候

logrotate -d /etc/logrotate.conf

有core,跟进源码去感觉是依赖bug,换到3.8.6终于ok了…回去看作者更新日志:

3.8.4 -> 3.8.5
     - Improved rotation during daylight saving time and between timezone
       changes.
     - Fixed ACL setting problem caused by ext3 erroneously reporting ENOSYS
       instead of ENOSUP.
     - Do not continue with rotation if state file is corrupted.
     - Make logrotate.status creation atomic.
     - Allow "hourly" rotation. See manpage for more information.
     - Use "/bin/echo" in tests. Fixes tests execution in Dash.
     - Do no try to parse config files bigger than 16MB.
     - Improved manpage consistency and formatting.
     - Fix race condition between acl_set_fd() and fchmod().

好久没写写自己心里的感受了。大概自从毕业工作之后,每天忙很多的事情,就不再有心情去体会一些事情。习惯了忙碌于工作,在这个城市的街头、地铁和公交站穿行,每天想的是五环的房价、今天有没有雾霾和路上堵不堵,只有夜深人静的时候才会想起以前的日子。

想起北航的参天大树,想起以前的同学,想起清华的荷塘,想起校园里美好的日子和可爱的女生,想起晚上安静的校园和校园外的车水马龙。大学的生活确实是人生最美好的几年,让你无论什么时候回想起来都觉得心底柔软、岁月静好。

有时候觉得自己迷失在时间里了。每天早晨醒来,想的都是匆匆逝去的时间和忙不完的事情。停下脚步,听见风穿过的声音,才觉得时间的洪流里面,也有这些许的温存,有难得的避风港,让灵魂停下来歇息一下。哎,我曾经是多么感性的一个人啊…

物是人非。尽量不去想这些事情。每每回忆以前都觉得会触动心里的痛点,看时光匆匆流去,那些逝去的画面,变得越发模糊,而又无可奈何。于是习惯了让工作占据思想,理性太多而湮没感性,这如何又不是一种躯壳。生活节奏已经进入快车道,每每停下来的时候都会觉得不安,这偶然的感性就变得异常奢侈。

入夜。这座熟悉而陌生的城市,又一次慢慢进入梦乡。同一面蓝天下的人啊,有人睡去,有人醒着,有人睡不着。窗外灯火阑珊,寂寞的人,愿你在时光中找到温暖。

作为获取文件名和文件路径的函数,dirname和basename的签名是:

#include <libgen.h>

char *dirname(char *path);

char *basename(char *path);

之前没注意的地方是这个函数的输入输出都不是const的,也就意味着这个函数调用过程可能会修改char*指向的string内容。所以直接输出一个不可变字符串是不行的,同理,也要考虑这个非const函数会破坏入参。也就是:

char *str = "/abc/def";
printf("%s\n", dirname(str));

会core掉。而

#include <stdio.h>
#include <libgen.h>
int main(int argc, const char *argv[]) {
  char str[] = "/abc/def";
  printf("%s\n", dirname(str));
  printf("%s\n", basename(str));
  printf("%s\n", str);
  return 0;
}

输出结果是:

/abc
abc
/abc

还有一个更有意思的问题….如果对同一个path先后调用dirname和basename,那么返回的只有一个是对的….因为源已经被修改了。反过来可以。

path在执行过程中被修改了。C系的函数很多面临这个问题,如果不这么做的话要申请一块额外的内存,然后返回,而释放这块内存的工作得调用方完成,这种情况下,内存泄露的可能性很大,所以很多c库函数的选择是用全局变量(getopt等)或者修改入参(basename等)。

最近在琢磨MVCC和悲观乐观锁的问题,感觉有些以前学习的点没有串联起来,主要是隔离级别和锁相关的,这里总结思考一下:

  1. 两阶段锁解决了什么问题

    两阶段锁主要解决事务调度的可串行化,保证了调度是正确的。一个简单的例子参考度娘这里。

  2. 实现两阶段锁对应了什么隔离级别。

    两阶段锁保证了基本的隔离级别正确性,RC之上的隔离级别(包含RC)都需要至少保证两阶段锁。一个例外是如果where条件不走索引的话,是可能全表加锁的,这种情况下mysql为了性能提前解锁了不满足条件的行,参见这里

  3. 各个隔离级别对应的加锁策略。

    这个比较简单了:

    • RU:读加S锁,写加X锁,完成即可释放。
    • RC:读加S锁,写加X锁,读锁完成可释放,写锁一直到事务完成再释放。
    • RR:读加S锁,写加X锁,读写锁都一直到事务完成再释放。
    • SE: RR基础上再加范围锁。
  4. select如何防止丢失更新。

    按照何登成博客里面的定义,可以区分MVCC下两种读。

    快照读

    • select * from table where ?;

    当前读。

    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert into table values (…);
    • update table set ? where ?;
    • delete from table where ?;

    快照读级别下,写事务可能丢失更新,因为select并不阻塞写,两个读写事务可能基于同一个快照点。当前读级别下,写阻塞读,所以涉及同一行的读写事务一定是串行的。不会丢失更新。

    基于乐观锁的方式下,也不会丢失更新,因为检查到更新可能被覆盖的操作都会回滚(打回重试)了。

  5. MVCC和锁(悲观乐观)的实现方式下,隔离级别是怎么实现的?

    MVCC主要针对冲突数据的处理,乐观锁、悲观锁决定了最终原子的更新一行的方式。

    MVCC加乐观锁的方式基本思路如下:

    定义一个keyValueSet,Conditional Update在此基础上加上了一组更新条件conditionSet { … data[keyx]=valuex, … },即只有在D满足更新条件的情况下才将数据更新为keyValueSet’;否则,返回错误信息。引用

    MVCC加悲观锁主要是提供了不加锁的读。按何登成的文章里看,就是快照读+当前读。快照读级别下,直接按照版本读就行,当前读级别下,如果有锁冲突还是要加锁。

    在ob里的实现上看比较明显,行的修改增量组织为一棵B树,历史版本表现为B树叶子节点上挂的链表。链表的按照版本号串接起所有历史版本,全局Publish version决定了当前可见的最新版本。

    在快照读级别下,select不需要加锁,只需要每次按照publish version去链表遍历,找到可见的结果并返回。如果不修改transaction consistency set的情况下,这种读取可能导致两次读结果不一致,不满足RR或者SI(Snapshot Isolation)。OB0.5增加了一个readonly snapshot的级别,可以提供对一个快照的只读操作,保证了多次读取的一致,但是没有snapshot级别不加锁的读写事务(快照写),毕竟基于一个旧的快照做写操作可能使新的提交丢失。这里要么类似乐观锁验证一下版本,要么加锁来延迟读写。

    当前读级别下,select也要加锁直到事务结束释放,跟mysql的实现一致。

    总结来讲,如果把当前读看成写事务的话,那么ob事实上是读操作只看版本号,写操作只看锁。如果只考虑当前读和写操作的话,那么相当于有冲突的时候读锁延迟了写操作,写锁延迟了读操作,保证了调度的串行。这种情况下,多次读取的结果是一致的。

  6. select for update在mvcc下如何实现。

    道理是一样的,跟mvcc没什么关系,select for update实际上相当于写事务(select的时候加写锁,直到事务结束再释放,update操作本身也是一样的过程,先检索符合条件的记录加锁,再修改并提交,这样才能保证是原子的)

  7. 分布式环境下的快照读

    单点的快照相对来讲比较容易,因为每次可以取一下publish version,来决定什么是可以读取的,但是分布式环境下,每个点读取的时候不能总去看全局publish version(似乎也可以), 这样本地读取的时候可能由于时间偏移出现全局本地开启事务的时间戳大于全局publish version的问题,

工作里遇到一个问题,想把mysql的crc直接封装一下让go来调用,因为查表的crc计算性能实在是不快,对我们这种文件系统的大报文计算来看,crc容易变成瓶颈。大概性能对比如下:

两线程crc32查表 O2优化
time_elapsed:471.474976s
total_size_m : 200000.000000M
crc rate : 424.200684m/s


两线程crc64指令 O2优化
time_elapsed:24.877853s
total_size_m : 200000.000000M
crc rate : 8039.278809m/s

c这一端的计算比较容易,go的crc默认是查表, 所以存在不兼容的问题。出于兼容和性能考虑,用cgo封装一下。

首先在c语言下把crc打包成lib库,考虑go的移植,直接用静态库比较好。go这边调用如下:

package s3crc

import "unsafe"

// #include <stdlib.h>
// #cgo CFLAGS: -I../../../../src/lib
// #cgo LDFLAGS: -L../../../../src/lib -lcrc
// #include "s3_crc64.h"
import "C"

func s3_crc64(buf string, len int64) uint64 {
    cbuf := C.CString(buf)
    defer C.free(unsafe.Pointer(cbuf))
    return uint64(C.s3_crc64(unsafe.Pointer(cbuf), C.int64_t(len)))
}

遇到的坑主要有:

  1. cgo不支持ccache,所以这个比较扯。习惯使用ccache的同学(好样的)建议export CC=gcc
  2. 所有类型都需要做转换。go的类型对应到c的类型之后,都在C命名空间下。
  3. 静态库路径需要指明,go的buffer映射到c下面之后,需要考虑释放的问题。参见文献

最近在总结一些协议这么设计的原因,比较杂,纯做记录:

  1. 选主过程相当于Paxos的prepare过程:

    选出的Leader得到了多数派的同意,相当于Paxos一阶段完成,由于当前Term下投过票的Server保证不再接受小于等于当前Term的,所以保证了选主最终只能选出一个。

    不同于Paxos的地方在于这一轮Term下,follower除非Lease过期,否则不会发起新的选主,也就是发起Epoc更大的提案。Raft可以认为MultiPaxos的一种应用,MultiPaxos可以认为是合并了Prepare阶段的SingleDegreePaxos.

  2. raft读一致性和选主timeout

    raft协议本身由于主只是续约了follower的lease,所以正常情况下follower不会在lease过期前发起新的选主,但是master网络分区情况下,master本身不维护自己的lease,follower lease过期会发起新的选主从而产生新的主。这时候旧主的写一定不能成功,但是旧主的读不受影响。

    以前在ob的时候阳老师设计的选主协议里面通过主lease来解决了这个问题,只是当时没体会到原因在这里。协议要求补充限制:主在自己的lease过期后放弃主的身份,主维持心跳的过程中续约follower的lease,根据rpc的返回,只有收到多数派的回应并且term没有更大的才续约自己的lease。新启动的server在follower状态至少等一个lease周期才发起选主,来让可能的旧主过期。

  3. paxos是两阶段提交的一种场景。

    两阶段提交的一阶段也是获得所有参与者的同意。只不过是所有而不是多数派,一阶段通过之后,相当于决议已经形成,所以后一阶段不会产生新的决议。2阶段可以完全异步来做,如果2阶段失败,不影响形成的决议。这意味着两阶段提交是非抢占式的。

上次见刘鹏老师可能还是搜狐实习的时候,后来刘鹏走了,峰扬走了,整个团队改做推荐系统,最后又回到效果广告,算是绕了一个圈吧。买了本计算广告,更多的是给自己一个念想,念念不忘必有回响么。

主要记录几个知识点或者观点:

  1. 竞价的历史性作用、gd广告和竞价广告的诞生原因

    保量(guarantee delivery)广告相对于品牌广告(CPT),主要解决了流量浪费的问题,CPM的售卖方式相对增加了广告流量的利用率(比如按照性别的售卖,大致可以增加20-25%的收益)。然而保量广告相对来讲对流量的利用并不极致,大部分gd系统都面临超卖或者保守售卖的问题,可能会导致补量或者浪费,在线流量分配和优化也是个比较复杂的问题。所以才有了竞价广告,根据Nash均衡,广告价格是出价方博弈的结果。这样就可以让广告效益最大化,区分优质流量,同时长尾的流量也有变现的机会。

    当然也就引入了程序化交易。程序化交易是工业化的结果,但是对于广告来讲,良莠不齐的广告对于场景融入是非常不利的。程序化交易在移动时代带来了更不好的用户体验。

  2. 搜索广告的场景和精妙之处

    搜索广告实现了两点最精巧的地方:精确判断用户意图,跟媒体内容完美融入,所以历来是兵家必争之地。

  3. 移动广告、gps定位、地理位置

    移动时代,广告不能照搬PC时代的经验。移动设备屏幕上广告的体验是很差的。移动时代对于用户意图的判断实际上是更精准了,比如可以通过GPS等判断用户所处的位置,推送更符合场景的广告。但是广告体验就更加困难了,这是有利有弊的地方。

  4. O2O是潜在的广告市场

    O2O商家是之前几乎不可能成为广告主的一个群体。O2O广告有相当的地域要求,所以对Targeting的要求更高,相对PC时代,移动时代才是O2O广告能发力的地方。智能手机的普及提供了更精准的Targeting,使这部分人群有可能成为潜在广告主。

Bibliography:

[1] 怎样向非专业人士专业地解释「纳什均衡」?, https://www.zhihu.com/question/19804990

今天遇到一个设计上没考虑好的问题,记录一下。

之前Libeasy的逻辑如果一个连接上有超时的报文的话,整个连接会destroy掉。考虑网络拥塞的情况,如果AB两个报文同时在等待发送,A报文先进入发送队列(链表,非TCP发送buffer),B后进入,而A超时时间长,B立即超时,那么清理掉B的待发送报文的时候,如果destroy掉连接,那么本来可以发送出去的A报文就被强制失败了。

考虑这种情况做了一点修改,让超时的报文被清理掉的时候不会destroy连接。这样编码的时候需要指定一个报文被编码出来的buffer是属于哪个会话(session)的,同时记录一下每个session对应的最后一个buffer位置。清理的时候可以从上述位置回溯到不属于当前session的buffer或者到头部为止。看似没啥问题。今天发现了如下的bug:

考虑如果一个会话编码的多个buffer(或者一个buffer)被部分发送完毕(write返回大小为准),这时候会话超时,这个会话所属的所有编码过的buffer都被干掉,但是连接并没有被destroy,之后的报文继续发送的话,客户端实际上收到了不完整的报文,相当于之后的TCP流都错位了。这种情况下,destroy连接是明显更安全的做法。

所以还是要考虑周全啊。

最近其实是乏善可陈的,项目差不多写完,修修补补,提升下性能。其他的事情诸多不顺,看房价蹭蹭的涨,看工资慢慢的爬,感情纠结一团,其他的么,牙疼,估计牙周炎犯了。

然后今天炎泼说要走了,陈闯二话没说也跟着走了,先知先觉的后知后觉的都有各自的想法,只是我比较懵逼,可能最近事情多了,精力都不在这上面。每个人有自己的选择,目前看互联网差不多进入稳定期了,风口还有,但是能飞起来的猪不多了,剩下的人,要么在大公司朝九晚五,要么在创业公司等期权等上市,少数人还追求着理想,不过最后如果没有回到前面两个状态的话,一般是创业去了。

工作几年觉得介于目的性明确和不明确之间吧。做技术的明显分了两派,以KPI为核心的和以兴趣为核心的,很多时候这两个不是不兼容的。目的明确的,一切跟个人发展不相关的都推掉,目的不明确的,还是选择follow heart多一点。没有褒贬,个人选择而已。最好是大方向下,找点自己喜欢做的,否则工作容易无趣啊。不过这种坑也似乎不好找,当然不排除有人兴趣特别,比如我就是向往资产阶级腐朽堕落的生活方式…

博客好久不写了。感觉技术文章吧,一旦开头容易虎头蛇尾,扯扯淡吧,微博微信朋友圈足够了。无聊的时候,也就是刷刷知乎找朋友扯扯淡,看看有没有新梗,翻墙看看。最近整个社会都压抑吧,连门缝里塞小纸片的都少了,可见人民群众都进入到了一种就这样吧还能咋滴的状态里。

所以我还是一直觉得要找到一点精神的寄托,哪怕是打游戏呢(没鄙视的意思打游戏挺好)。最近听许巍的新歌《生活不止眼前的苟且》,觉得似乎又回到校园了,回到北航的主楼,回到绿园的树荫和荷塘,想起陈旧的宿舍楼里阳光洒在斑驳的墙面,想起清华园的午后,想起月下的荷塘,想起东门外五道口的火车道…

    生活不止眼前的苟且
    还有诗和远方的田野
    你赤手空拳来到人世间
    为找到那片海不顾一切

所以我这么感性的人是不是应该去搞音乐…毕竟钢琴比键盘也就是多了几十个键而已,用惯了Emacs的话,和弦弹起来也没什么按不过来的。

都是幻觉,都是幻觉…

最近在做http返回的时候发现go的xml生成(marshal)会把引号转义,如下:

    <?xml version="1.0" encoding="UTF-8"?>

    <Part>
      <PartNumber>1</PartNumber>
      <LastModified>2016-03-18T08:24:25.000Z</LastModified>
      <ETag>&#34;0c78aef83f66abc1fa1e8477f296d394&#34;</ETag>
      <Size>12121</Size>
    </Part>     

看了下源码,marshal函数的实现就会默认转义。这样就只能加一个Type,不直接用string,然后定义这个Type的marshl函数。上网搜了一下发现可以找个方法绕过去:struct的修饰可以指明当前的struct field不做转义,直接输出。

    type Part struct {
         XMLName      xml.Name `xml:"Part"`
         PartNumber   int
         LastModified string
         ETag         string `xml:",innerxml"`
         Size         int64
    }

这样可以直接在序列化的时候传自己拼成的ETag值。比如:

     Part{PartNumber: 1,
         LastModified: S3TimeFormat(GetCurrentTime()),
         ETag:         `<ETag>"acbd18db4cc2f85cedef654fccc4a4d8"</ETag>`,
         Size:         12121}

输出结果满足要求:

    <?xml version="1.0" encoding="UTF-8"?>

    <Part>
      <PartNumber>1</PartNumber>
      <LastModified>2016-03-18T08:32:39.000Z</LastModified><ETag>"acbd18db4cc2f85cedef654fccc4a4d8"</ETag>
      <Size>12121</Size>
    </Part>

换了个ubuntu的环境想编译一下vim,为了防止不兼容手动编译了python2.7,之后把改过的vim源码放上去编译发现有问题:

undefined symbol: PyUnicodeUCS4_AsEncodedString

于是换回标准的vim源码还是一样的问题,我擦嘞。上网搜一下说Python模式是UnicodeUCS2的支持,从源码里面直接grep一下这个函数发现是有的:

Include/unicodeobject.h:# define PyUnicode_AsEncodedString PyUnicodeUCS4_AsEncodedString

那只可能是没有开启编译选项了,./configure —help发现有如下一项:

  --enable-unicode[=ucs[24]]
                      Enable Unicode strings (default is ucs2)

configure到ucs4重新编译python2.7,完美。没毛病。

本文源码以libev4.20为准,其他版本大同小异。

libev是广泛使用的事件库,是一个功能强大的reactor,可以把Timer、IO、进程线程事件放在一个统一的框架下进行管理。如果有其他的事件触发需求也可以改libev源码把该事件加入libev的框架中(当前前提是得理解libev的设计)。有文章说libev性能比libevent好,没实验过,但是从源码角度看,libev要更简洁,当然更费解一点。作者为了追求代码的整洁和统一使用了大量的宏,造成了阅读的不便。这里我们从宏观分析一下libev的设计实现,然后穿插分析一些小的trick。旨在学习总结libev设计中优雅的地方。

基本概念

首先是一些主要的概念和数据结构。

libev通过定义watcher来关注一个事件,并且把事件类型和对应的毁回调函数关联起来。libev定义了多种事件类型,同时可以在框架中自己添加感兴趣的事件,libev保证了事件触发的顺序性,并在多线程环境下保证事件的串行触发。

每一种类型的watcher都包含几个基本的成员,通过EV_WATCHER和EV_WATCHER_LIST宏实现。EV_WATCHER_LIST比EV_WATCHER多了一个纸箱下一个watcher的指针。EV_WATCHER_TIMER是定时器的基类,多一个timestamp。这几个宏这里留一个小的trick分析,在后面阐述。

首先修改/ssh/sshd_config,把Port 22解注释,然后加一行Port xx。之后修改 /etc/sysconfig/iptables,加入该端口的Rules:

-A INPUT -m state --state NEW -m tcp -p tcp --dport xx -j ACCEPT  

重启即可。

/etc/init.d/sshd restart

这么做的目的是如果出现问题,还有一个端口可以上去修改。登录的时候需要对应的指定一下端口:

ssh -p xx user@host
scp -P xx ... user@host

补充两个SSH技巧:

  1. 客户端配置中转,主要是通过跳板机登录:

    Host xx
    HostName 192.168.1.1
    User xx
    ProxyCommand ssh -q xxx@login2.xxx.xx nc %h %p
    
  2. 保持会话。 ssh会在.ssh目录下生成一个会话选项,下次登录同一个server公用会话,不需要验证。

    Host *
    ControlMaster auto
    ControlPath ~/.ssh/master-%r@%h:%p
    

    上面会话共享,所以不能关闭会话。可以通过 ssh -fN xxx 把第一个会话放到后台不退出

某天ReviewBoard客户端突然用不了,使用 rbt post的时候报错:

from six.moves.urllib.parse import quote
ImportError: No module named urllib.parse

乍一看以为什么包被卸载了。于是pip install six —upgrade,无果。pip uninstall RBTools再重新安装RBTools,也无效。

查了一下six是python2、python3的兼容包,直接修改源码,不要兼容了,发现用到的地方好多,改不过来(ps,兼容python2、python3真不容啊)。

查看six的版本,发现跟本地一样的,本地没什么问题。说明不是six的问题。直接在命令行from six.moves.urllib.parse import quote,发现本地ok,服务器上不行。

这就比较扯了,同样的版本,本地可以服务器不行。直接卸了重装:

pip uninstall six #注,这里卸载了1.9的six
pip uninstall six #日,还有一个1.2的six,不知道pip list为啥显示不出来。
pip install six
easy_install RBTools

然后就ok了。所以还是要习惯在virtualenv下搞啊…

acceptor: a([epoc:4, value:n]) b[epoc:3,value:y] c[epoc:6,:value:y]

proposor: x5, x7, (x4, x3, x6)

prepare:x5[a, b], x7[b, c]

commit:x[4,n] y[6, y]

一共有5个proposer,

1)初始时 x4 (prepare)–> a, x3 (prepare)–> b,x6 (prepare)–> c, 2) x4, x3, x6 挂掉 3)x5(prepare)–> a, b 4)x5(commit[epoc:4, value:n])–>a, b 5) x7(prepare) –>b, c 6) x7(commit[epoc:6, value:y])–>b, c

gitbook是nodejs实现的用来帮助书写电子书的,很多开源的书籍项目是基于gitbook的。gitbook的编写语言是markdown,书籍遵循一定的格式完成之后可以通过gitbook发布为各种版本,比如pdf,mobi等电子书格式,或者发布为静态的website,挂到github pages上,对于一些开源项目来说,这是很方便的书写帮助文档的方式。gitbook同样有一个paas的平台,可以允许多人协作在线完成一部电子书

首先安装gitbook

npm -g install gitbook
npm -g install gitbook-cli
npm -g install ebook-convert

第三个是安装生成电子书的插件,但是这里并不会安装bin文件,需要手动安装Calibre。Mac下可以安装Calibre的app然后链接一下bin:

ln -s /Applications/calibre.app/Contents/MacOS/ebook-convert /usr/local/bin

这样就可以生成pdf/mobi等格式的电子书了。

使用方式可以通过gitbook help来查看。主要是如下几个:

gitbook build [book] [output]   生成电子书,通过--format指定输出格式,默认输出为website
gitbook pdf [book] [output]     生成pdf电子书
gitbook epub [book] [output]    生成epub电子书
gitbook mobi [book] [output]    生成mobi电子书
serve [book]                    生成并开启http server预览
init [directory]                根据summary建立基本目录结构
install [book]                  安装依赖和插件

Summary的基本样子见这里:

# Summary
* [Introduction](README.md)
* [入门](getting_started/README.md)
   * [初识](getting_started/what_is_it.md)
   * [安装](getting_started/installing_es.md)
   * [API](getting_started/api.md)
   * [文档](getting_started/document.md)
   * [索引](getting_started/tutorial_indexing.md)
   * [搜索](getting_started/tutorial_search.md)
   * [汇总](getting_started/tutorial_aggregations.md)
   * [小结](getting_started/tutorial_conclusion.md)
   * [分布式](getting_started/distributed.md)
   * [本章总结](getting_started/conclusion.md)
* [分布式集群](distributed_cluster/README.md)

工作两周年了,感觉这两年走了不少弯路,也学到不少东西,最近看到日照的周年总结,觉得也应该写写。而且看看日照当时的情况,觉得跟我现在也差不多,同样处在职业的选择期,一个明确的规划还是很重要的。

总结

2013年进入支付宝Oceanbase组,待了一年半的时间吧,第一年主要在0.5上修修改改,第二年换到了UPS组,参与1.0的设计。从第一年的情况看,基本是完成了一个不错的职业素养培训,有了分布式的基本概念,处于了解熟悉的阶段。第二年开始对分布式协议有了一些认识,算是入门了吧。总结两年还是学到了一些东西,但是也算是付出了一些代价。出来之后看之前的组,觉得对应届生还是个不错的选择,ob有点像一个学校,闲的时候有不错的学习机会,就是加班太多了。

后来身体原因离职,来搜狐大数据中心,开始做推荐系统相关的东西。从那个时候想做算法,搞了半年多吧,有些了解,不算入门,大概处于这样一个状态。对我来说,兴趣总归是第一位的,然而在一个方向上有所积累更重要。这样容易过了几年发现什么都会点什么都不精通。互联网发展这么快,

log4cxx是apache基金会的log开源项目,log4j的c++实现,安装的时候遇到一个小问题:

首先正常安装:

wget http://mirrors.cnnic.cn/apache/logging/log4cxx/0.10.0/apache-log4cxx-0.10.0.tar.gz
tar zxvf apache-log4cxx-0.10.0.tar.gz
cd apache-log4cxx-0.10.0
./configure
make

报错libdb-4.3.so 格式错误。开始以为是文件损坏了,查了一下这个文件

yum provides */libdb-4.3.so

提示在db4里面,重新安装db4

yum reinstall db4

回去还是报错。后来看了一下格式,貌似链接到了32位版本。修改软连接:

cd /usr/lib/
rm libdb-4.3.so
ln -s ../../lib64/libdb-4.3.so .

同样处理另外一个库

rm libexpat.so
ln -s ../../lib64/libexpat.so.0.5.0 libexpat.so

应该就OK了。

最近突然想到这个东西,以前Oceanbase的UpdateServer提交的时候是做了group commit的。基本思路如下:

  1. 并发小的时候,超过一个时间窗口就直接提交
  2. 并发大的时候,等请求填满一个buffer再一起提交。

实现上可以考虑如下伪代码:

while true:
    start timer
    if buffer full or timer reach limit:
        commit
        reset timer
    else
        wait and receive

广泛运用在一些需要组合请求的地方,比如一些rpc因为历史原因(嗯)一直单条请求,不能改协议的情况下,可以考虑转发一下,做个缓冲。当然这种情况下一般是通过队列来缓冲请求压力。

事实上我也只是带实习生而已。但是感觉上之前更多的是把自己的任务分出去,而现在是开始跟一些人一起完成一个任务。主要的不同是之前做的事情是在个人工作量里面,自己做也只是慢一点,有时候甚至自己做反倒比分出去更快。现在做的事情已经超过一个人工作量了,必须发挥团队的能动性了,而且之前也只是对自己的工作负责,现在你要负责的是一个团队的KPI了。

主要有几点感觉:

  1. 分配好工作。需要能力的,需要经验的,需要稳定细心的,每个人的风格不一样,应该做的工作也不一样。
  2. 规范流程。大部分加班或者事故都是能从流程上避免的,在设计开发测试上省下来的时间最终都会在上线修bug处理问题的过程中补回来。
  3. 做好计划和项目管理。做一个包含不确定性和风险的事情最重要的是路线图。实现的过程就是把抽象的东西一点点具象化的过程。所以计划上要能够自顶向下,逐步求精。把写程序的思路用进来。
  4. 最后就是能力的问题了。统筹考虑是种需要一点点培养的能力。这个也就只能慢慢来了。何况只要不是纯做管理,你就一定有自己的工作,自己的KPI,协调好自己工作和他人工作安排的关系,还是非常重要的。

最后补一个我理解的开发流程图:

一道题目,本来觉得挺简单的,后来卡在一个小问题上。mark一下:

给定一个数据库表,存了所有话题的关系,形式是:parent->child,表示前面是后面话题的父话题。根据这个关系构建出话题树并打印。

eg:
输入:
    a b
    c a
    d e
    e f
    r c
    r d
输出:
     r
       c
         a
           b
       d
         e
           f
题目隐含:
    1. DAG:有向无环
    2. 节点不重复

直观看类似于Graphviz的算法,只不过保证了是棵树。C++实现上可以直接通过树来做,这里用了Python。

Phibricator是Facebook开源的一体化项目管理、代码review工具,主要特点是集成度高、界面漂亮。根据搭建的经验看,确实做的非常完善,各种细节用起来像是商业产品,不会像其他的开源产品那么难以配置。这里把配置过程根据回忆写一下:

官方给出过一个一键安装的脚本,但是下载的时候感觉有点问题吧,下载完不是sh而是网页,而且我只有一台机器,环境都已经预先有了,也不想再搭一套LAMP。这里我们以centos5为例:

首先安装httpd + mysql + php,由于centos5的版本太低,yum源里的mysql和php均低于phibricator要求的版本,我们需要先升级yum源,参见参考文献, 不过好像安装的时候没有php53u,而是直接php53

之后把对应的扩展也装了:

yum install php53-mbstring
yum install php53-mysql