日请求亿级的 QQ 会员 AMS 平台 PHP 7 升级实践

QQ 会员活动运营平台( AMS ),是 QQ 会员增值运营业务的重要载体之一,承担海量活动运营的 Web 系统。 AMS 是一个主要采用PHP语言实现的活动运营平台, CGI 日请求3亿左右,高峰期达到8亿。然而,在之前比较长的一段时间里,我们都采用了比较老旧的基础软件版本,就是 PHP5.2+Apache2.0(2008年的技术)。尤其从去年开始,随着 AMS 业务随着 QQ 会员增值业务的快速增长,性能压力日益变大。

于是,自2015年5月,我们就开始规划 PHP 底层升级,最终的目标是升级到 PHP 7。那时, PHP 7尚处于研发阶段,而我们讨论和预研就已经开始了。

一.PHP 7的学习和预研

1. HHVM 和 JIT

2015年就 PHP 性能优化的方案,有另外一个比较重要的角色,就是由 Facebook 开源的 HHVM(HipHop Virtual Machine ,HHVM 是一个 Facebook 开源的 PHP 虚拟机)。HHVM 使用 JIT(Just In Time,即时编译是种软件优化技术,指在运行时才会去编译字节码为机器码)的编译方式以及其他技术,让 PHP 代码的执行性能大幅提升。据传,可以将 PHP5 版本的原生 PHP 代码提升5-10倍的执行性能。

HHVM 起源于 Facebook 公司,Facebook 早起的很多代码是使用 PHP 来开发的,但是,随着业务的快速发展, PHP 执行效率成为越来越明显的问题。为了优化执行效率,Facebook 在2008年就开始使用 HipHop,这是一种PHP执行引擎,最初是为了将 Fackbook 的大量 PHP 代码转成 C++,以提高性能和节约资源。使用 HipHop 的PHP 代码在性能上有数倍的提升。后来,Facebook 将 HipHop 平台开源,逐渐发展为现在的 HHVM。

HHVM 成为一个 PHP 性能优化解决方案时,PHP7还处于研发阶段。曾经看过部分同学对于 HHVM 的交流,性能可以获得可观的提升,但是服务运维和 PHP 语法兼容有一定成本。有一阵子,JIT 成为一个呼声很高的东西,很多技术同学建议 PHP 7也应该通过 JIT 来优化性能。

2015年7月,我参加了中国 PHPCON,听了惠新宸关于 PHP7 内核的技术分享。实际上,在2013年的时候,惠新宸( PHP7内核开发者)和 Dmitry (另一位PHP语言内核开发者之一)就曾经在 PHP5.5的版本上做过一个 JIT 的尝试(并没有发布)。PHP 5.5的原来的执行流程,是将 PHP 代码通过词法和语法分析,编译成 opcode 字节码(格式和汇编有点像),然后,Zend 引擎读取这些 opcode 指令,逐条解析执行。

而他们在opcode环节后引入了类型推断(TypeInf),然后通过JIT生成ByteCodes,然后再执行。

于是,在 benchmark (测试程序)中得到非常好的结果,实现JIT后性能比 PHP 5.5提升了8倍。然而,当他们把这个优化放入到实际的项目 WordPress (一个开源博客项目)中,却几乎看不见性能的提升。原因在于测试项目的代码量比较少,通过 JIT 产生的机器码也不大,而真实的 WordPress 项目生成的机器码太大,引起 CPU 缓存命中率下降(CPU Cache Miss)。

总而言之,JIT 并非在每个场景下都是点石成金的利器,而脱离业务场景的性能测试结果,并不一定具有代表性。

从官方放出Wordpress的 PHP 7和 HHVM 的性能对比可以看出,两者基本处于同一水平。

2. PHP7在性能方面的优化

PHP 7是一个比较底层升级,比起 PHP 5.6的变化比较大,而就性能优化层面,大致可以汇总如下:

  • 将基础变量从struct(结构体)变为union(联合体),节省内存空间,间接减少CPU在内存分配和管理上的开销。
  • 部分基础变量(zend_array、zend_string等)采用内存空间连续分配的方式,降低CPU Cache Miss的发生的概率。CPU从CPU Cache获取数据和从内存获取,它们之间效率相差可以高达100倍。举一个近似的例子,系统从内存读取数据和从磁盘读取数据的效率差别很大,CPU Cache Miss类似遇到缺页中断。

  • 通过宏定义和内联函数(inline),让编译器提前完成部分工作。无需在程序运行时分配内存,能够实现类似函数的功能,却没有函数调用的压栈、弹栈开销,效率会比较高。

3. AMS平台技术选型的背景

就提升PHP的性能而言,可以选择的是2015年就可直接使用的HHVM或者是2015年底才发布正式版的PHP7。会员AMS是一个访问量级比较大的一个Web系统,经过四年持续的升级和优化,积累了800多个业务功能组件,还有各种PHP编写的公共基础库和脚本,代码规模也比较大。

我们对于PHP版本对代码的向下兼容的需求是比较高的,因此,就我们业务场景而言,PHP7良好的语法向下兼容,正是我们所需要的。因此,我们选择以PHP7为升级的方案。

二.PHP7升级面临的风险和挑战

对于一个已经现网在线的大型公共Web服务来说,基础公共软件升级,通常是一件吃力不讨好的工作,做得好,不一定被大家感知到,但是,升级出了问题,则需要承担比较重的责任。为了尽量减少升级的风险,我们必须先弄清楚我们的升级存在挑战和风险。于是,我们整理了升级挑战和风险列表:

  • Apache2.0和PHP5.2这两个2008-2009年的基础软件版本比较古老,升级到Apache2.4和PHP7,版本升级跨度比较大,时间跨度相差7-8年,因此,兼容性问题挑战比较高。实际上,我们公司的现网PHP服务,很多都停留在PHP5.2和PHP5.3的版本,版本偏低。

  • AMS大量使用自研tphplib扩展,tphplib很早在公司内部就没有人维护了,这个扩展之前只有PHP5.3和PHP5.2的编译so版本,并且,部分扩展没有支持线程安全。支持线程安全,是因为我们以前的Apache使用了prefork模式,而我们希望能够使用Apache2.4的Event模式(2014年中,在prefork和worker之后,推出的多进程线程管理模式,对于支持高并发,有更良好的表现)。
  • 语法兼容性问题,从PHP5.2到PHP7的跨度过大,即使PHP官方号称在向下兼容方面做到99%,但是,我们的代码规模比较大,它仍然是一个未知的风险。

  • 新软件面临的风险,将Apache和PHP这种基础软件升级到最新的版本,而这些版本的部分功能可能存在未知的风险和缺陷。

部分同学可能会建议采用Nginx会是更优的选择,的确,单纯比较Nginx和Apache在高并发方面的性能,Nginx的表现更优。但是就PHP的CGI而言,Nginx+php-ftpm和Apache+mod_php两者并没有很大的差距。另一方面,我们因为长期使用Apache,在技术熟悉和经验方面积累更多,因此,它可能不是最佳的选择,但是,具体到我们业务场景,算是比较合适的一个选择。

三.版本升级实施过程

1. 高跨度版本升级方式

从一个2008年的Apache2.0直接升级到2016年的Apache2.4,这个跨度过于大,甚至使用的http.conf的配置文件都有很多的不同,这里的需要更新的地方比较多,未知的风险也是存在的。于是,我们的做法,是先尝试将Apache2.0升级到Apach2.2,调整配置、观察稳定性,然后再进一步尝试到Apach2.4。所幸的是,Apache(httpd)是一个比较特别的开源社区,他们之前一直同时维护这两个分支版本的Apache(2.2和2.4),因此,即使是Apache2.2也有比较新的版本。

于是,我们先升级了一个PHP5.2+Apache2.2,对兼容性进行了测试和观察,确认两者之间是可以比较平滑升级后,我们开始进行Apache2.4的升级方案。

PHP5.2的升级,我们也采用相同的思路,我们先将PHP5.2升级至PHP5.6(当时,PHP7还是beta版本),然后再将PHP5.6升级到PHP7,以更平滑的方式,逐步解决不同的问题。 于是,我们的升级计划变为:
Apache2.4编译为动态MPM的模式(支持通过httpd配置切换prefork/worker/event模式),根据现网风险等实时降级。
Prefork、Worker、Event三者粗略介绍:

PHP5.2的升级,我们也采用相同的思路,我们先将PHP5.2升级至PHP5.6(当时,PHP7还是beta版本),然后再将PHP5.6升级到PHP7,以更平滑的方式,逐步解决不同的问题。 于是,我们的升级计划变为:

Apache2.4编译为动态MPM的模式(支持通过httpd配置切换prefork/worker/event模式),根据现网风险等实时降级。

  • prefork,多进程模式,1个进程服务于1个用户请求,成本比较高。但是,稳定性最高,不需要支持线程安全。
  • worker,多进程多线程模式,1个进程含有多个worker线程,1个worker线程服务于1个用户请求,因为线程更轻量,成本比较低。但是,在KeepAlive场景下,worker资源会被client占据,无法响应其他请求(空等待)。
  • event,多进程多线程模式,1个进程也含有多个worker线程,1个worker线程服务于1个用户请求。但是,它解决了KeepAlive场景下的worker线程被占据问题,它通过专门的线程来管理这些KeepAlive连接,然后再分配“工作”给具体处理的worker,工作worker不会因为KeepAlive而导致空等待。

关于Event模式的官方介绍:

http://httpd.apache.org/docs/2.4/mod/event.html

(部分同学可能会有event模式不支持https的印象,那个说法其实是2年多以前的国内部分技术博客的说法,目前的版本是支持的,详情可以浏览官方介绍)

开启动态切换模式的方法,就是在编译httpd的时候加上:

--enable-mpms-shared=all

从PHP5.2升级到PHP5.6相对比较容易,我们主要的工作如下:

  • 清理了部分不再使用的老扩展
  • 解决掉线程安全问题
  • 将cmem等api编译到新的版本
  • PHP代码语法基于PHP5.6的兼容(实际上变化不大)
  • 部分扩展的同步调整。apc扩展变为zend_opcache和apcu,以前的apc是包含了编译缓存和用户内存操作的功能,在PHP比较新版本里,被分解为独立的两个扩展。

从PHP5.6升级到PHP7.0的工作量就比较多,也相对比较复杂,因此,我们制定了每一个阶段的升级计划:

  • 技术预研,PHP7升级准备。
  • 环境编译和搭建,下载相关的编译包,搭建完整的编译环境和测试环境。(编译环境还是需要比较多的依赖so)。
  • 兼容升级和测试。PHP7扩展的重新编译和代码兼容性工作,AMS功能验证,性能压测。
  • 线上灰度。打包为pkg的安装包,编写相关的安装shell安装执行代码(包括软链接、解决一些so依赖)。然后,灰度安装到现网,观察。
  • 正式发布。扩大灰度范围,全量升级。

因为从PHP5.2升级到PHP5.6的过程中,很多问题已经被我们提前解决了,所以,PHP7的升级主要难点在于tphplib扩展的编译升级。

涉及主要的工作包括:

  • PHP5.6的扩展到PHP7.0的比较大幅度改造升级(工作量比较大的地方)。
  • 兼容apcu的内存操作函数的改名。PHP5的时候,我们使用的apc前缀的函数不可用了,同步变为apcu前缀的函数(需要apcu扩展)。

  • 语法兼容升级。实际上工作量不算大,从PHP5.6升级到PHP7变化并不多。我们大概在2016年4月中旬份完成了PHP7和Apache的编译工作, 4月下旬进行现网灰度,5月初全量发布到其中一个现网集群。

2. 升级过程中的错误调试方法

在升级和重新编译PHP7扩展时,如果执行结果不符合预期或者进程core掉,很多错误都是无法从error日志里看见的,不利于分析问题。可以采用以下几种方法,可以用来定位和分析大部分的问题:

  • var_dump/exit

从PHP代码层逐步输出信息和执行exit,可以逐步定位到异常执行的PHP函数位置,然后再根据PHP函数名,反查扩展内的实现函数,找到问题。这种方法比较简单,但是效率不高。

  • gdb –p/gdb c这种方法主要用于分析进程core的场景,我们采用的编译方式,是将mod_php(PHP变成Apache的子或块的方式),使用gdb –p来监控Apache的服务进程。

命令:ps aux|grep httpd

gdb调试指定进程:命令:gdb -p
使用c进行捕获,然后构造能够导致core的web请求:
Apache通常是多进程模式,为了让问题比较容易复现,可以在http.con里修改参数,将启动进程数修改为1个(下图中的多个参数都需要调整,以达到只启动单进程单线程的目的)。
当然还有一种更简单的方法,因为Apache本身就支持单进程调试模式的。./apachectl -k start -X -e debug然后再通过gdb –p来调试就更简单一些。

gdb调试指定进程:

命令:gdb -p

使用c进行捕获,然后构造能够导致core的web请求:

Apache通常是多进程模式,为了让问题比较容易复现,可以在http.con里修改参数,将启动进程数修改为1个(下图中的多个参数都需要调整,以达到只启动单进程单线程的目的)。

当然还有一种更简单的方法,因为Apache本身就支持单进程调试模式的。

./apachectl -k start -X -e debug

然后再通过gdb –p来调试就更简单一些。

  • 通过strace命令查看Apache进程具体在做了些什么事情,根据里面的执行内容,分析和定位问题。

strace -Ttt -v -s1024 -f -p pid(进程id)

备注:执行这些命令,注意权限问题,很可能需要root权限。

四.PHP5.6到PHP7.0扩展升级实践记录

1. 数据类型的变化

  • zval

php7的诞生始于zval结构的变化,PHP7不再需要指针的指针,绝大部分zval**需要修改成zval*。如果PHP7直接操作zval,那么zval*也需要改成zvalZ_*P()也要改成Z_*()ZVAL_*(var, …)需要改成ZVAL_*(&var, …),一定要谨慎使用&符号,因为PHP7几乎不要求使用zval*,那么很多地方的&也是要去掉的。

ALLOC_ZVALALLOC_INIT_ZVALMAKE_STD_ZVAL这几个分配内存的宏已经被移除了。大多数情况下,zval*应该修改为zval,而INIT_PZVAL宏也被移除了。

/* 7.0zval结构源码 */
/* value字段,仅占一个size_t长度,只有指针或double或者long */
typedef union _zend_value {
    zend_long         lval;                /* long value */
    double            dval;                /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

struct _zval_struct {
    zend_value        value;            /* value */
    union {
        。。。
    } u1;/* 扩充字段,主要是类型信息 */
    union {
        … …
    } u2;/* 扩充字段,保存辅助信息 */
};
  • 整型

直接切换即可:

long->zend_long

/* 定义 */
typedef int64_t zend_long;
/* else */
typedef int32_t zend_long;
  • 字符串类型

PHP5.6版本中使用 char*+ len的方式表示字符串,PHP7.0中做了封装,定义了zend_string类型:

struct _zend_string {
    zend_refcounted_h gc;
    zend_ulong        h;                /* hash value */
    size_t            len;
    char              val[1];
};

zend_stringchar*的转换:

zend_string *str;
char *cstr = NULL;
size_t slen = 0;
//...
/* 从zend_string获取char* 和 len的方法如下 */
cstr = ZSTR_VAL(str);
slen = ZSTR_LEN(str);
/* char* 构造zend_string的方法 */
zend_string * zstr = zend_string_init("test",sizeof("test"), 0);

扩展方法,解析参数时,使用字符串的地方,将‘s’替换成‘S’:

/* 例如 */
`zend_string` `*zstr`;
if (zend_parse_parameters(ZEND_NUM_ARGS() , "S", &zstr) == FAILURE)
{
    RETURN_LONG(-1);
}
  • 自定义对象

源代码:

/* php7.0 zend_object 定义 */
struct _zend_object {
    zend_refcounted_h gc;
    uint32_t          handle;
    zend_class_entry  *ce;
    const zend_object_handlers  *handlers;
    HashTable        *properties;
    zval              properties_table[1];
};

zendobject是一个可变长度的结构。因此在自定义对象的结构中,zendobject需要放在最后一项:

/* 例子 */
struct clogger_object {
    CLogger *logger;
    zend_object  std;// 放在后面
};
/* 使用偏移量的方式获取对象 */
static inline clogger_object *php_clogger_object_from_obj(zend_object *obj) {
    return (clogger_object*)((char*)(obj) - XtOffsetOf(clogger_object, std));
}
#define Z_USEROBJ_P(zv) php_clogger_object_from_obj(Z_OBJ_P((zv)))
/* 释放资源时 */
void tphp_clogger_free_storage(zend_object *object TSRMLS_DC)
{
    clogger_object *intern = php_clogger_object_from_obj(object);
    if (intern->logger)
    {
        delete intern->logger;
        intern->logger = NULL;
    }
    zend_object_std_dtor(&intern->std);
}
  • 数组

7.0中的hash表定义如下,给出了一些注释:

/*7.0中的hash表结构 */
typedef struct _Bucket { /* hash表中的一个条目 */
zval              val;   /* 删除元素zval类型标记为IS_UNDEF */
zend_ulong        h;                /* hash value (or numeric index)   */
zend_string      *key;              /* string key or NULL for numerics */
} Bucket;        
typedef struct _zend_array HashTable;    
struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    reserve)
        } v;
        uint32_t flags;
    } u;
    uint32_t          nTableMask;
    Bucket           *arData; /* 保存所有数组元素 */
    uint32_t          nNumUsed; /* 当前用到了多少长度, */
    uint32_t          nNumOfElements; /* 数组中实际保存的元素的个数,一旦nNumUsed的值到达nTableSize,PHP就会尝试调整arData数组,让它更紧凑,具体方式就是抛弃类型为UDENF的条目 */
    uint32_t          nTableSize; /* 数组被分配的内存大小为2的幂次方(最小值为8) */
    uint32_t          nInternalPointer;
    zend_long         nNextFreeElement;
    dtor_func_t       pDestructor;
};

其中,PHP7在zend_hash.h中定义了一系列宏,用来操作数组,包括遍历key、遍历value、遍历key-value等,下面是一个简单例子:

/* 数组举例 */
zval *arr;
zend_parse_parameters(ZEND_NUM_ARGS() , "a", &arr_qos_req);
if (arr)
{
    zval *item;
    zend_string *key;
    ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(arr), key, item) {
        /* ... */
    }
}
/* 获取到item后,可以通过下面的api获取long、double、string值 */
zval_get_long(item) 
zval_get_double(item) 
zval_get_string(item)

PHP5.6版本中是通过zend_hash_find查找key,然后将结果给到zval **变量,并且查询不到时需要自己分配内存,初始化一个item,设置默认值。

2. PHP7中的api变化

  • duplicate参数 PHP5.6中很多API中都需要填入一个duplicate参数,表明一个变量是否需要复制一份,尤其是string类的操作,PHP7.0中取消duplicate参数,对于string相关操作,只要有duplicate参数,直接删掉即可。因为PHP7.0中定义了zval_string结构,对字符串的操作,不再需要duplicate值,底层直接使用zend_string_init初始化一个zend_string即可,而在PHP5.6中string是存放在zval中的,而zval的内存需要手动分配。

涉及的API汇总如下:

add_index_stringadd_index_stringladd_assoc_string_exadd_assoc_stringl_exadd_assoc_stringadd_assoc_stringladd_next_index_stringadd_next_index_stringladd_get_assoc_string_exadd_get_assoc_stringl_exadd_get_assoc_stringadd_get_assoc_stringladd_get_index_stringadd_get_index_stringladd_property_string_exadd_property_stringl_exadd_property_stringadd_property_stringlZVAL_STRINGZVAL_STRINGLRETVAL_STRINGRETVAL_STRINGLRETURN_STRINGRETURN_STRINGL

  • MAKE_STD_ZVAL

PHP5.6中,zval变量是在堆上分配的,创建一个zval变量需要先声明一个指针,然后使用MAKE_STD_ZVAL进行分配空间。PHP7.0中,这个宏已经取消,变量在栈上分配,直接定义一个变量即可,不再需要MAKE_STD_ZVAL,使用到的地方,直接去掉就好。

  • ZEND_RSRC_DTOR_FUNC
修改参数名rsrc为res
/* PHP5.6 */
typedef struct _zend_rsrc_list_entry {
    void *ptr;
    int type;
    int refcount;
} zend_rsrc_list_entry;
typedef void (*rsrc_dtor_func_t)(zend_rsrc_list_entry *rsrc TSRMLS_DC);
#define ZEND_RSRC_DTOR_FUNC(name)        void name(zend_rsrc_list_entry *rsrc TSRMLS_DC)

/* PHP7.0 */
struct _zend_resource {
    zend_refcounted_h gc;/*7.0中对引用计数做了结构封装*/
    int               handle;
    int               type;
    void             *ptr;
};
typedef void (*rsrc_dtor_func_t)(zend_resource *res);
#define ZEND_RSRC_DTOR_FUNC(name) void name(zend_resource *res)

PHP7.0中,将zend_rsrc_list_entry结构升级为zend_resource,在新版本中只需要修改一下参数名称即可。

  • 二级指针宏,即Z_*_PP

PHP7.0中取消了所有的PP宏,大部分情况直接使用对应的P宏即可。

  • zend_object_store_get_object被取消

根据官方wiki,可以定义如下宏,用来获取object,实际情况看,这个宏用的还是比较频繁的:

static inline user_object *user_fetch_object(zend_object *obj) {
    return (user_object *)((char*)(obj) - XtOffsetOf(user_object, std));
}
/* }}} */ 
#define Z_USEROBJ_P(zv) user_fetch_object(Z_OBJ_P((zv)))
  • zend_hash_exists、zend_hash_find

对所有需要字符串参数的函数,PHP5.6中的方式是传递两个参数(char* + len),而PHP7.0中定义了zend_string,因此只需要一个zend_string变量即可。

返回值变成了zend_bool类型:

/* 例子 */
zend_string * key;  
key = zend_string_init("key",sizeof("key"), 0);
zend_bool res_key = zend_hash_exists(itmeArr, key);

五.AMS平台升级PHP7的性能优化成果

现网服务是一个非常重要而又敏感的环境,轻则影响用户体验,重则产生现网事故。因此,我们4月下旬完成PHP7编译和测试工作之后,就在AMS其中一台机器进行了灰度上线,观察了几天后,然后逐步扩大灰度范围,在5月初完成升级。

这个是我们压测AMS一个查询多个活动计数器的压测结果,以及现网CGI机器,在高峰相同TGW流量场景下的CPU负载数据:

就我们的业务压测和现网结果来看,和官方所说的性能提升一倍,基本一致。

AMS平台拥有不少的CGI机器,PHP7的升级和应用给我们带来了性能的提升,可以有效节省硬件资源成本。并且,通过Apache2.4的Event模式,我们也增强了Apache在支持并发方面的能力。

六.小结

我们PHP7升级研发项目组,在过去比较长的一个时间段里,经过持续地努力和推进,终于在2016年4月下旬现网灰度,5月初在集群中全量升级,为我们的AMS活动运营平台带来性能上大幅度的提升。

PHP7的革新,对于PHP语言本身而言,具有非凡的意义和价值,这让我更加确信一点,PHP会是一个越来越好的语言。同时,感谢PHP社区的开发者们,为我们业务带来的性能提升。

文章来源公众号:小时光茶社(Tech Teahouse)

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏BaronTalk

Android 模块化探索与实践

首发于《程序员》杂志五月刊 前言 万维网发明人 Tim Berners-Lee 谈到设计原理时说过:“简单性和模块化是软件工程的基石;分布式和容错性是互联网的...

4129
来自专栏葡萄城控件技术团队

ActiveReports 报表应用教程 (12)---交互式报表之贯穿钻取

在葡萄城ActiveReports报表中提供强大的数据分析能力,您可以通过图表、表格、图片、列表、波形图等控件来实现数据的贯穿钻取,在一级报表中可以通过鼠标点击...

2016
来自专栏数据和云

静默错误:Oracle 数据库是如何应对和处理的 ?

说明:关于本文提到的所有参考文档,一律上传分享,关注本公众号回复 122arch 获得。

782
来自专栏数据库

Github推荐:MySQL DBA不可错过的五大开源管理工具!

对于数据库管理员(DBA)来说,保持数据库运行在最佳状态需要具备敏捷,专注,快速反应的能力以及一颗冷静的头脑。数据库几乎是所有应用程序成功运行的核心,由于DBA...

36610
来自专栏运维

服务稳定性及应用防护方案

日志收集推荐使用Elastic Stack协议栈,可以满足收集海量日志需求,而且便于后续分析、报表、报警操作

361
来自专栏美团技术团队

基于 Appium 的 Android UI 自动化测试

自动化测试是研发人员进行质量保障的重要一环,良好的自动化测试机制能够让开发者及早发现编码中的逻辑缺陷,将风险前置。日常研发中,由于快速迭代的原因,我们经常需要在...

3774
来自专栏我的博客

Lucenu和Sphinx介绍

一、Lucene介绍 1、简介 Lucene 是apache软件基金会一个开放源代码的全文检索引擎工具包,是一个全文检索引擎的架构,提供了完整的查...

3206
来自专栏张善友的专栏

BlackPearl 的 ServiceObject 开发部署

K2“BlackPearl”提供一个平台,用于管理和利用现有的跨越任何数量的业务系统的业务信息,以降低成本。它考虑到决策的制定,线路的排定以及报告,并根据360...

1799
来自专栏SAP最佳业务实践

SAP最佳业务实践:ETO–项目装配(240)-7预先采购

CNMM预先采购 image.png 必须在项目执行的一开始就执行预先采购。ProMan 用于控制活动。 角色项目经理 后勤 ® 项目系统 ® 物料 ® 执行 ...

3307
来自专栏java一日一条

2016 年 7 个最佳的 Java 框架

毫无疑问,Java是目前最需要的编程语言之一。在这里,我们已经挖掘了一些关于框架趋势的有用信息,以减轻全球软件开发人员的日常工作。

481

扫码关注云+社区