C++软件架构选型

基础服务 (开源)

项目 推荐☆☆☆☆☆ 推荐☆☆☆☆ 推荐☆☆☆
虚拟化 podman docker
虚拟机集群 k8s
分布式存储 ceph

中间件

项目 推荐☆☆☆☆☆ 推荐☆☆☆☆ 推荐☆☆☆
名字服务/服务发现 consul
分布式协调一致性 etcd zookeeper
消息队列 kafka rabitmq
监控 prometheus open-falcon Zabbix
监控展示 grafana
日志存储 druid elastic-search Opentsdb
分布式任务调度 Elastic-Job
kv缓存服务 redis
kv数据库 leveldb mongodb
向量检索引擎 faiss milvus proxima

组件

项目 推荐☆☆☆☆☆ 推荐☆☆☆☆ 推荐☆☆☆
RPC框架 BPRC thrift
IDL protobuffer thrift json
基础库 boost poco
log glog Spdlog
malloc jemalloc tcmalloc
json fastjson jsoncpp
xml tinyxml2
yaml yaml-cpp
kv缓存 rockdb

分支路径图调度框架在vivo效果广告业务的落地实践

大家好,我是vivo效果广告预估服务的架构师刘作程。非常荣幸能在2022 vivo 技术大会和大家见面,我今天带来的分享是《分支路径图调度框架在vivo效果广告业务的落地实践》。

在介绍分支路径图调度框架之前,请容我先向大家介绍vivo效果广告预估服务。

vivo效果广告实时在线服务,是提供实时AI算法推荐的服务。在广告投放场景,承载了一天百亿级别数量的请求,支持VIVO广告收入。在可用性、可扩展性等方面具有非常高的要求。服务中起到调度作用的模块,上下游依赖特别多,比如特征服务、ABT实验平台、实时数据流、模型计算模块等等。调度模块在请求下游服务的方式都是采用异步的方式。那么我们是怎么管理这么多异步请求的呢?

异步调用已成为系统设计中的主流方法。虽然异步调度提升了系统性能,提升了资源的利用率,但却对系统的可扩展性和可维护性提出了挑战。回忆历史中用过的异步管理方法有以下三种。

面向过程方法,树调度,有限有向图管理。单纯使用面向过程的方法,简单却粗放,随着下游服务增多,代码逻辑中产生大量的callback函数和类。使得系统调度过程繁杂无序,可扩展性和可维护性变差。

树调度方法,使得异步调度进入框架调控的新阶段。可扩展性方面较面向过程的方法要好很多。但树结构不能准确描述复杂服务的调用流程。

有限有向图,是目前使用最为广泛的方法。扩展性较好,能管理复杂的调用流程。但这是否是一种完美的方法呢?不是的。有限有向图,对图中节点是全路径访问,对分支路径的管理不够友好。因此我们依然要探索新的方法。

为了解释有限有向图在实时在线服务中的局限性,我们以vivo效果广告预估服务的调度流程为例,进行说明。把调度流程经过抽象后,调度流程如左图一样简洁明了,但这只是理想状态。

那么现实状况又是什么样子的呢?大家来看,和所有的实时在线服务一样,我们在系统设计时,为了系统健壮,总要和大量的异常和超时做斗争。并且除了异常和超时,系统还需要有兜底逻辑。上一个简洁明了的有限有向图已经不复存在。

为了进一步说明,有限有向图不能完全把控在线服务中异步调度的流程。我们依据刚才展示的流程调度图,做了一个状态转换图。在这个图中,展示了系统中各个状态的流转路径,总数达到了7条之多。有限有向图是一种全路径图调度框架,已经难以适用复杂度不断增长的系统。

全路径图调度框架具体有什么痛点,逼迫让我们去寻找新的方法。具体原因就在这里,有限有向图在落地实践中,使用skip状态变量约束路径。系统在处理一次任务的过程中,不经过的路径上的所有节点状态都会被设置为skip。

全路径的图调度框架中,每增添一个节点,会导致:控制变量数量 +1,状态全集 x2, 复杂度成指数增长。在如此状态下,对复杂系统添加流程或调整流程,就会变成工程师的噩梦。设计开发时必须小心甚微,上线时则如履薄冰。

为了安全开发和提升迭代效率,新的调度方法被迫切地创建出来。那就支持分支路径的图调度框架。它的原理,则是在原有的图调度框架中,添加两处功能,一是加入了分支节点,二是对于图中节点的触发和激活支持“与”激活和“或”激活。是不是像极了逻辑电路呢?

大家是否有疑问,添加两处修改就可以了吗?这样做真的有效吗?其实依据,就来自于我们的《编译原理》里的常见概念和常见规律。

有限有向图,是一种NFA,即不确定的有限自动机。我们都知道,在实践中NFA实现难度很大,它不如DFA简洁和简单。所以结论呼之欲出,支持分支路径的图调度框架,则是把图变成DFA。我们还给它起了新名字,DDAG。

实践是检验真理的唯一标准,我们再通过实际的落地过程看,分支路径调度框架是否满足我们的预期。

我们回忆一下vivo效果广告预估服务的调度流程,那一个充满着异常、超时、兜底逻辑的调用图。为了便于说明,我们把它化简了一下,变成一个流程图,如左图,目前它还是一个全路径的调度图。使用单路径调度框架改造后,变成右图,图中添加了判断节点,具体路径的走向则由判断逻辑来控制。大量Skip的状态控制变量依然不复存在。 图中蓝、青、红分别代表了3条路径,让路径和流程一目了然。

当图与实时在线系统融合在一起的时候,我们发现了单路径图调度框架更多的提升空间。 比如,图的整体的超时、异常管理,图中节点的超时、异常管理, 以及复杂图结构的自动化简,会成为我们以后进一步升级的空间。

单路径图调度框架在VIVO效果广告预估服务中做了一次成功的实践。它帮助工程师降低在开发过程中的风险,提速在项目中的迭代效率。让算法预估服务的飞轮越转越快!(握拳)

Google protobuf使用技巧和经验总结

技巧 & 经验

性能优化

把repeated message结构尽可能摊平为基础类型的repeated字段

基础类型的repeated字段,包含 repeated int32, int64, float,double,bool等,但不包含string、bytes、message

比如:

message Item {
    int32 id = 1;
    int32 score = 2;
}

message R {
    repeated Item items = 1;
}

改为下面的设计,会提升序列化和反序列效率

message R {
    repeated int32 item_id = 1;
    repeated int32 item_score = 2;
}

原理是非string的基础类型的repeated字段,在申请内存时pb会申请连续线性大块内存,效率高;而message 的repeated字段,会按对象逐个去申请空间。

这种设计还有一个好处就是不会触发调用析构函数,如果采取 repeated Item 这种结构,会触发大量的析构函数,浪费 CPU 与 时间。(by chengyuxuan_yutian@163.com)

善用arena管理内存

  • arena对基础类型,比如int32, int64, float,double,bool等管理效率优化明显
  • arena不会管理string类型的内存申请(更高的版本已经支持,待验证)。

用固定长度repeated uint32 替换字符串

字符串是一种不定长的数据结构,内存管理方式成本较高。对于定长的字符串,通过转换成repeated uint32类型,可以获得更高效的管理。

除此之外,repeated uint32 也支持由arena管理。

善用Any类型

假设3个网络服务的调用关系如下:
A->B->C。
其中存在某些pb结构仅会由B透传给C,而B不需要解析,则可以把这些pb放入定义为any类型的字段中。

善用浅拷贝机制(set_allocated_xxx/release)

CopyFrom是深拷贝,若要实现浅拷贝则可以通过 set_allocated_xxx/release 两个函数进行控制

结合arena使用浅拷贝机制(unsafe_arena_set_allocated)

set_allocated_xxx的风险在于,pb析构的时候会把元素也析构掉,无法重复利用。且在一些特殊场景,在无法控制pb析构而不能使用release函数。这些场景可能是pb的析构工作由框架控制,旧的代码封装层次太深等。

这种情况可以使用unsafe_arena_set_allocated_xxx 避开这个问题。

使用陷阱

不要有交叉依赖

举例一C++系统中,模块A依赖 PB;模块B依赖PB,而模块C依赖A和B。则编译模块C时一定要同时编译A、B、PB。

利用protobuf一些特性来规避陷阱

良好的可扩展性 & 保留未定义字段

良好的可扩展性使得protobuf更好地向后兼容。上游更新了proto,新增字段,下游虽然没有更新proto文件,但是新增的字段依然可以保留,来自上游的字段可以透传给下游。拼接下游请求的结构pb时,尽可能使用CopyFrom,避免把字段逐个set。

使用编号定位存储的字段

为了更好地向后兼容,应该避免修改proto文件中现有字段的名字、类型。需要修改时,通过追加新字段(字段编号增加),弃用旧字段的方式。

故障相关

protobuf被广泛使用,饱经业界考验,如果遇到问题,绝大多数还是自身软件设计的问题。遇到问题,首先不应该怀疑protobuf,应该把视角集中到去发现自身的系统设计缺陷中。

一次内存泄露的故障排查

现象:

公司里一个c++网络服务中, PV较低时没有内存泄露;而PV较高,cpu idle降到30%以下,开始内存泄露,直到OOM。

排查过程:

用了 tcmalloc和gperf,逐步定位是protobuf 申请 repeated字段的空间,没有及时释放。repeated字段约1k~1w的规模。然后逐步缩小范围。

结果:

竟然是释放内存,都放到了一个线程中。当流量大时,单个线程计算能力成为瓶颈,内存释放变慢,表现为内存泄漏。

总结

protobuf释放的代价较大,不要全部的protobuf只放在一个线程操作。场景距离,lru cache的过期元素剔除。

https://www.cnblogs.com/zgwu/p/10403939.html

服务迁移至Kubernetes实践总结

梗概

Kubernetes相比传统的集群管理方式,表现出更多优势,因而国内互联网公司也都已经大规模使用 Kubernetes。

Kubernetes 拥有大量的用户群体和落地实践,做为生产环境的编排系统在成熟度和稳定性上已得到业界的认可。对于开发工程师来讲,把服务部署到k8s中,将会是运维工作的常态。

把服务迁移到K8S集群之前,需要对服务本身进行一些升级,对K8S进行适配。本文对升级过程中的一些常见问题,以及注意事项进行总结。

把服务迁移到K8S的意义是什么?

先说K8S有哪些特性呢?依据官方文档,有自我修复、弹性伸缩、自动部署和回滚、服务发现和负载均衡、机密和配置管理、批处理等特性。

这些特性无疑解放了开发工程师运维服务器集群的人力,可以把节省出的精力投入到系统设计、编码开发等价值更高的工作当中。

不要只看收益,不看风险。

K8S带给我们诸多收益的同时,也要注意到K8S给我们带来新的问题。

容器漂移

容器漂移是最常见的问题。容器在一台机器关闭,然后再另外一台机器上被启动。发生容器漂移的原因有滚动发布、物理机故障被回收、手工关闭容器、K8S集群进行碎片整理等。

容器漂移会给系统带来的负面影响有,节点一段时间内不可用、IP地址变化、缓存丢失、临时数据文件丢失等。

资源碎片

总有机器的一部分小块资源被闲置,却又不能满足有大块资源需求的服务。资源碎片会影响K8S集群的机器利用率,严重时会影响服务的发布。

资源隔离不完善-IO不能隔离

目前容器技术,没有实现对磁盘IO的资源隔离,因此同一宿主机上如果存在两个磁盘IO密集型的服务,有很大概率会相互影响。

尽管当前的容器技术已经实现对网络IO资源的隔离,但是可能受限于K8S的版本,以及公司内部CICD系统不提供对网络IO隔离的设置,因此对于网路IO资源密集的服务,部署到K8S时也要谨慎处理。

不能隔离的 IO 类型有:

  • 内存带宽
  • 网络带宽
  • 文件带宽

root权限

一些服务会调用只能root才有权限访问的接口,比如mlock。这些服务如果迁移到K8S,需要重新进行设计,因为公司里的CICD服务往往不会允许k8s中的容器以root用户运行。

系统调用失效

物理机和容器里面的接口发生了很大变化,举例说明:

  • CPU核数,获取物理机器核数的接口要替换成获取容器CPU核数的接口
  • 可用内存大小,获取物理机器内存大小的接口要替换成获取容器内存大小的接口

集群机器性能差异

k8s集群中,服务器可能来自不同厂商,采购的新旧型号不同,配件性能也可能会有差异。尤其不同cpu型号的性能差别巨大。而K8S不能把这种性能差异对应用变得透明。

一般方法是:

  • 添加机型约束
  • 把资源量化,比如1core 等于100算力(K8S暂未实现此功能)。

把服务迁移到K8S之前需要做哪一些升级?

支持容器化运行和部署

K8S本身就是容器的编排系统,所以服务要能在容器中运行起来。需要编写Dockerfile或docker-compose.yaml文件来实现编译镜像。

服务本身要具有良好的可伸缩性

服务本身需要具有容错机制,其上游也要有失败访问后的重试。除此之外,服务检测、坏点摘除、服务发现,也是必不可少的,所以需要把服务接入名字服务。

支持日志的集中采集

不同于传统的运维方法,我们难以进入每一个容器来查看进程的运行状况。把服务要接入日志中心的系统,在日志中心查阅、追踪系统状态是最好的选择。

监控指标集中上报

和把日志上报到日志中心的原因相同,把服务的监控指标,上传到监控平台/系统。而且为了减少依赖,尽可能使用SDK通过网络的方式上传监控指标,避免通过采集容器内日志文件的方式上报。

数据分片

对于一些数据分片的服务,需要有一个统一的分片管理模块。

可用性

K8S集群不总是可用的,我们在设计服务时,就要考虑对服务进行同城双活、异地多活等方式的部署。为了保证服务的可用性,把服务部署到同城不同K8S集群或者异地不同的k8s集群当中。

其他的注意事项

服务的日志自动清理,不然宿主机的磁盘会被用满,引发故障。

使用分布式文件系统来共享文件,比如ceph、OSS等。

程序异常时产生的coredump文件保留下来,以便排查问题。

自动弹性扩缩容

这是k8s的一个能大大节省运维工作,以及提升资源利用率的功能。

但自动弹性扩容在某些场景下会影响服务的可用性。以故障的灾后恢复为例,有问题的服务,被上线后,CPU负载降低,被自动缩容;问题修复后,开始重新扩容,扩容服务的时间可能是漫长的(可能加载内存数据慢、可能机器较多),服务不可用的时间也会变长。

总结

K8S也是一把双刃剑,一方面把开发工程师从繁琐的运维工作中解放出来;另一方面如何运维部署在K8S上面的服务,K8S较传统方法有较大的不同,开发工程师面对K8S这个新工具,要不断学习和总结。

C++无锁编程之自旋锁(spinlock)的实现

此实例通过c++11实现。

#pragma once

#include 
#include 

class Spinlock {
public:
    Spinlock() : flag(ATOMIC_FLAG_INIT), wait_count(2000) {}

    void lock() {
        int64_t i = 0;
        while(flag.test_and_set(std::memory_order_acquire)) {
            __asm__ ("pause");
            if (++i > wait_count) {
                sched_yield();
                i = 0;
            }
        }
    }

    bool try_lock() {
        if (flag.test_and_set(std::memory_order_acquire)) {
            return false;
        }
        return true;
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }

private :
    std::atomic_flag flag;
    int32_t wait_count;
};

C++无锁编程之AsyncParallelTask框架

简介
AsyncParallelTask框架,是为了适用于Rank3.0的拆包合包业务场景而设计和实现的异步任务调度框架,且具有良好的通用性和可移植性。

Rank拆包合包的业务场景,即Rank3.0接受到请求后,拆包往下游预估服务分发请求,在接收到返回后合并打分结果,最终把结果返回给上游。

使用AsyncParallelTask框架的优点
拆包合并由框架控制,免去了自己控制拆包后多个子任务的状态管理。
无锁化,没有锁竞争,性能高。
提供超时管理机制,有助于增强系统稳定性。
拥有友好的API,使用简单。
AsyncParallelTask框架可适用的场景举例
需要拆包合包的预估服务,比如Rank模块
搜索引擎的merger模块
其他需要拆包合包的业务场景
设计
设计思想
使用异步IO方式,不会引起线程阻塞,且通过超时控制来避免长尾任务。
通过使用原子变量和原子操作(atomic)来控制计数、状态变化。
支持多线程,且逻辑实现不使用任何种类的锁,使用lockfree数据结构和线程间通信机制来保证线程安全。
通过使用C++11标准的新特性,比如仿函数、参数模板等,来对外提供简洁和更加友好的API。
类域设计
AsyncParallelTask框架,总共包含控制器AsyncTaskController、定时器TimerController、异步并行任务类AsyncParallelTask、分发子任务类AsyncParallelSubTask等4部分组成。

控制器AsyncTaskController
AsyncTaskController是AsyncParallelTask的调度器,提供调度AsyncParallelTask的接口。内部包含线程池、定时器。

当AsyncTaskController调度AsyncParallelTask的运行时,首先把AsyncParallelTask放入线程池中调度,然后启动对AsyncParallelTask的超时监控。

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

使用最小堆来管理延时任务和周期任务
使用1个timerfd配合epoll来实现定时任务的激活
使用1个eventfd配合epoll来实现定时任务的添加
使用一个LockFree的栈,实现生产者消费者模型。由外部多线程写,TimerController内部1个线程来读。
TimerController内部流程图

异步任务基类AsyncTask
任何继承AsyncTask的类都可供AsyncTaskController进行调度。

AsyncTask中定了一个基本的接口和AsyncTask的状态的转换。

部分代码举例:

class AsyncTask {
public:
enum Status {
UNSCHEDULED,
PROCESSING,
WAIT_CALLBACK,
CALLBACK,
TIMEOUT,
EXCEPTION,
FINISHED
};

AsyncTask() :
    id(0),
    parent_id(0),
    timeout_threshold(0),
    status(UNSCHEDULED) {}
virtual ~AsyncTask() {}
virtual Status process() = 0;
virtual Status timeout() { return TIMEOUT; }
virtual void destroy() {}
virtual void reset() {
    id = 0;
    parent_id = 0;
    timeout_threshold = 0;
    status = UNSCHEDULED;
}

virtual void callback() {}
virtual void callbackExcepiton() {}
virtual void callbackTimeout() {}

…….
private:
int64_t id;
int64_t parent_id;
int32_t timeout_threshold; // millisecond;
std::atomic status;
};
AsyncTask的状态转换图
AsyncTask约束了异步任务的7种状态,和8中状态转换。其中TIMEOUT和EXCEPITON是等效的,为了方便区分两种异常而设置两个状态。

并行任务AsyncParallelTask
AsyncParallelTask内部流程图

并行子任务AsyncParallelSubTask
拆包后分发操作主要在AsyncParallelSubTask中执行。需要创建AsyncParallelSubTask的子类,实现其中分发操作和合并结果的操作。

使用举例
初始化AsyncTaskController
在进程Init中执行,全局单例。设置分发的线程池。

static ThreadPool thread_pool(config.getWorkerThreadNum()); 
auto& task_controller = Singleton::GetInstance();
task_controller.setThreadPool(&thread_pool);

定义AsyncParallelSubTask的子类PredictAsyncParallelSubTask 
主要实现process和mergeResult两个函数,具体参考

https://gitlab.vmic.xyz/iai_common/rank/blob/experiment3/task/predict_async_parallel_subtask.h

https://gitlab.vmic.xyz/iai_common/rank/blob/experiment3/task/predict_async_parallel_subtask.cpp

class PredictAsyncParallelSubTask : public AsyncParallelSubTask {
public:
PredictAsyncParallelSubTask() :
alg_info(nullptr),
context(nullptr),
split_info({0}) {}

virtual ~PredictAsyncParallelSubTask() {}

virtual Status process() {
    if (nullptr == context) {
        throw std::runtime_error("context is nullptr");
    }
    if (nullptr == alg_info) {
        throw std::runtime_error("alg_info is nullptr");
    }
    PredictService::asyncRequestZeusServer(this, *context, *alg_info, split_info);
    return WAIT_CALLBACK;
}

virtual void mergeResult();

virtual void reset() {
    AsyncParallelSubTask::reset();
    alg_info = nullptr;
    split_info = {0};
    context = nullptr;
    temp_res.Clear();
}

void collectResult(const zeus::proto::ZeusResponse& res) {
    auto& zeus_res = const_cast(res);
    temp_res.mutable_item()->Swap(zeus_res.mutable_item());
    temp_res.mutable_model_response()->Swap(zeus_res.mutable_model_response());
}

void setAlgInfo(AlgInfo* alg_info) { this->alg_info = alg_info;};
void setRankContext(RankContext *context) { this->context = context;}
void setSplitInfo(SplitInfo& split_info) { this->split_info = split_info;}

private:
void praseZeusToScoreItem(TargetCode code, double score, ScoreItem *score_item);

AlgInfo* alg_info;
RankContext *context;
SplitInfo split_info;
zeus::proto::ZeusResponse temp_res;

};
创建AsyncParallelTask
具体参考

class PredictRankTask : public AsyncTask {
public:
……
private:
AsyncParallelTask parallel_task;
……
};

……
for (int32_t partition_id = 0; partition_id < partition_count; ++partition_id) {
int64_t total_count = req_item_size;
int64_t offset = split_count * partition_id;
int64_t end = offset + split_count;
end = end > total_count ? total_count : end;
SplitInfo split_info({total_count,
split_count,
partition_count,
partition_id,
offset,
end});

            auto sub_task = std::make_shared();
            sub_task->setAlgInfo(const_cast(&alg_info));
            sub_task->setSplitInfo(split_info);
            sub_task->setRankContext(&getContext());
            parallel_task.addSubTask((std::shared_ptr)sub_task);
        }

……
auto task = this;
parallel_task.setAllDoneCallback([=]() {
task->response();
task->setStatusCallback();
});

    parallel_task.setIncomplateCallback([=]() {
            task->response(Error::E_INCOMPLATE, "some predict server is error!");
            task->setStatusCallback();
            });

    parallel_task.setAllFailCallback([=]() {
            task->response(Error::E_PREDICT_ALL_FAILED, "all predict server is error!");
            task->setStatusCallback();
            });

    int32_t timeout = PredictService::getMaxTimeout(scene_id, sub_alg);
    parallel_task.setTimeoutCallback(timeout, [=]() {
            task->response(Error::E_PREDICT_ALL_TIMEOUT, "all predict server timeout!");
            task->setTimeout();
            });

    auto& task_controller = Singleton::GetInstance();
    parallel_task.setController(&task_controller);
    parallel_task.setId(task_controller.generateUniqueId());
    setStatusWaitCallback(std::memory_order_relaxed);
    task_controller.scheduleImmediately(¶llel_task);

执行调度
task_controller.scheduleImmediately会在当前线程分发拆包到线程池。而task_controller.schedule则会在线程池中选择一个线程分发。

    auto& task_controller = Singleton::GetInstance();
    parallel_task.setController(&task_controller);
    parallel_task.setId(task_controller.generateUniqueId());
    setStatusWaitCallback(std::memory_order_relaxed);
    task_controller.scheduleImmediately(¶llel_task);

编码
源码地址:

测试
压力测试
测试机器为 2017H2-A1-1, 32 core机器

QPS cpu num of items body length session latency P99 session latency AVG
client latency AVG

bandwidth mem remark
300 56% 1W 200KB 43 35 40 3.4 Gb/s 1%
1600 62% 2k 40KB 31 21.6 24 3.64Gb/s 1.1%
稳定性测试
测试方法:
CPU 60%的压力下,持续测试24小时。 

测试结果:
Rank服务可稳定提供服务。无内存泄露。

极限测试
测试过程:

缓慢把CPU压力从30%提升到90%~100%之间,并维持10分钟,然后把cpu压力降低至60%。整个过程中观察Rank稳定性,有无内存泄露。

测试结果:

CPU压力达到90%以上时,Rank内存增长,超时错误日志变多,定时器失准,返回大量超时、错误。 

Cpu压力降低至60%之后,Rank服务恢复正常,内存使用率变小,无内存泄露,超时错误日志不再新的产出。

符合预期。

打分一致性测试
测试方法:
使用rank-diff工具,从passby环境,复制两份流量请求新旧rank生产环境,分别记录打分结果。最后通过python脚本统计打分结果差异。

测试结果:
1000qps,新旧rank打分一致,差异率小于99.9%,满足需求。

产生差异的数据,分为两种。1)为打分近似值,差别0.01以下。 2)打分无效取默认值0.001.

有锁Rank和无锁Rank性能对比
2k条广告时,1600qps,有锁和无锁Rank压力测试性能对比
测试机器  CPU 32 cores,其中QPS、带宽都是相同的。

有锁
无锁
remark 
QPS 1600
相同
CPU 54.1% 63%
session latency AVG 15 21.7
session latency P99 21 31
bandwidth 3.64Gb/s 3.64Gb/s 相同
req body length 40 KB 40 KB 相同
Context Switch

C++-双缓冲(DoubleBuffer)的设计与实现

源码如下:

#pragma once

#include 

template
class DoubleBuffer {
    public:
        DoubleBuffer() : cur_index(0) {}

        T& getWorkingBuffer(std::memory_order order = std::memory_order_seq_cst) {
            return buffers[cur_index.load(order)];
        }

        T& getBackupBuffer(std::memory_order order = std::memory_order_seq_cst) {
            return buffers[1 ^ cur_index.load(order)];
        }

        void switchBuffer(std::memory_order order = std::memory_order_seq_cst) {
            cur_index.fetch_xor(1, order);
        }

    private:
        T buffers[2];
        std::atomic cur_index;
};

使用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两个文件里面。

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

梗概

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

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

脑图

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

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

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

其他资料

图搜引擎服务器磁盘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性能基线标准。

参考资料

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

内容梗概

  • 从简单到复杂,推演后台服务的底层通信框架的进化过程,包括网络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计算瓶颈,增强并发能力。但我认为提升成本大,效果小,单线程更合理些。

广告引擎技术总结

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

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

广告引擎特点

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

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

详细阐述,请看这里

业务

  • 业务的抽象

架构

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

详细阐述,请看这里

算法

  • 竞价排名
  • 机器学习

trouble shoot

性能测试

  • 压力测试
  • IO测试

监控

  • 系统监控

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

  • 业务监控

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

运维

测试

团队

  • 流程管理

使用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

工程师手记-将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

对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

实验推导结论

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

安全的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 测试

测试环境的搭建

必须多套测试环境

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

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

发布

灰度发布

开源消息队列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日

线程安全的单例模式-以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;

总结

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

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群里的讨论。

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

  • 避免数据脏读
  • 多级缓存的一致性协议

系统的鲁棒性

  • 在缓存系统停止服务,但仍能保证整体系统正常运行。因此在使用缓存之前,检查提供缓存系统的有效性。

单机缓存

  • 单机缓存是指,将系统资源存放于每个单台服务器上,而不是集中存储与分布式缓存系统中。 缺陷是,如果原数据发生更改,为保证一致性,则必须调用每一台服务器清理或更新缓存。

软件设计之浮点运算

工程项目中,需要注意浮点数的点

  • 不能有 == 比较
  • 上下游注意nan值检查

计算机对浮点的表示和运算的特点

二进制表示

  • 部分十进制小数转化为二进制小数后,是无限循环小数,比如 0.05 转换为二进制 0.000011001100110011001100110011…… 如果截断,实际计算机存储的是 0.049999999999

IEEE 754 标准下的浮点

  • 小数位数是精确的
  • 有效位数是有限的

计算机做浮点运算的陷阱和缺陷

  • 丢失精度,近似比较不准确

正确的比较方法 : if (a - b < 0.0000001 && a-b > -0.0000001) {}

  • 四舍五入,导致误差累积

软件设计时如何保证高精度的计算

使用高精度计算库,比如:

  • Java 使用BigInteger 、BigDecimal 类
  • PHP 使用bcmath扩展提供的方法
  • C/C++ 自行编写高精度算法和数据结构

附录

优秀文档总结:https://gywbd.github.io/posts/2015/9/floating-point-number.html

MVC框架-路由分发总结

路由的类型 Route Type

无路由 No Route

HTTP请求直接定位到特定的脚本文件执行。比如http://domain/news/latest.php

动态路由

依据某种动态规则进行路由和分发,一般有如下形式:

强约束URL

将Class,function 等信息隐藏在url中,请求来时,依据规则解析URL就可以定位Action函数,比如http://domain/modelName/className/functionName

优点是简单,缺点是灵活性差,路由信息包含的少,比如拦截器、模版只能在action中写代码调用,不能集中配置。

依据注释信息自动路由

将路由信息,写到执行函数的注释里,请求来时,解析所有action函数的注释,找到合适的,再执行action函数。 Java Spring 和 PHP Symonfy 支持这种路由方式

注释的关键字有(举例)

  • @url
  • @template
  • @filter
  • @trigger

优点是维护起来比路由表简单,缺点是性能较低。

静态路由

路由配置写在配置文件中,依据配置文件,确定如何分发。

路由信息配置在配置文件中,文件或数据格式如下:

  • PHP
  • Yaml
  • Xml
  • Json

笔者最推荐的是 Yaml,因为Yaml 格式的可读性最高,最易于维护。

半静态半动态路由

动态和静态相结合的路由.

尤其是在URL解析中,使用正则匹配的路由方式。

路由的组成 Route Components

路由分发的元素

  • URL , URL特征决定分发到特定Action函数
  • Action 函数,函数所在的class、Namespace
  • 模版 template ,页面渲染的模版
  • 拦截过滤器,Filters , 比如权限检查、地区检查、无效请求检查等各类filter
  • 触发器,triggers , 页面调用后触发的操作

URL 的组成

URL 包含着路由的关键信息,决定请求分发到具体的Action函数

MVC 框架的 URL 一般由下列部分组成

  • 域名 domain name
  • 模块/目录 model/directory name
  • 功能 function name
  • 参数 parameter

参数有两种形式存于http请求中,一种是作为POST和GET方法的参数,一种是直接在URL中。

Route Style

Restfull 的路由分发

遵循Rest 风格、规范的URl

普通的路由分发

企业应用架构之分层 – 总结

常见分层架构模式

三层架构 3-tier architecture

微软.net 体系推荐的分层结构,因此早期在C/S架构和ASP编码的系统中被广泛应用,同时也被其他语言广泛借鉴。

表现层, Presentation Layer(PL)

主要负责数据的输入接口和输出。输入指在WEB、客户端或为外界提供的API的数据请求接口;输出则是Web界面、客户端输出、API的数据输出。

页面模版、 对外API数据格式化、Request接受 、Output推送、 Commander操作都在这一层。

业务逻辑层, Bisiness Logic Layer(BLL)

主要负责从原始数据到结果数据的中间过程。系统中最关键、中重要的一层。也被称作领域层(Domain Layer),领域中所有对象的逻辑关系和功能实现算法都在这一层中。

业务逻辑、服务等处于这一层。

数据访问层, Data Access Layer(DAL)

主要是对原始数据的操作层(CRUD操作),为业务逻辑层或表示层提供数据服务。数据源包括,数据库、文件、网络存储系统、其他系统开放的API、程序运行上下文环境等等。

许多框架中的ORM 、 Active Record 、 Dao 类或库都处于这一层。

三层架构总结

三层架构能较好的满足大部分的业务场景,实现高内聚、低耦合。不同层次之间逻辑解耦或者隔离,变为弱依赖,每层的逻辑内聚。从层次角度看,使系统有了较好的可扩展性。

三层架构的不足有:

  • 业务逻辑复杂度高的系统,业务逻辑层将变得庞大臃肿,为了解决这个问题,四层架构/多层架构被人提出。
  • 不关注表现层的实现。

四层架构 4-tier architecture

四层架构和三层架构极为类似,分为表示层、服务层、业务逻辑层、数据访问层。除了服务层,其他三层和三层架构的三层几乎一致,而服务层,是对三层架构中业务逻辑层的再细分,以解决业务逻辑层经常出现的臃肿的问题。

四层架构是Java EE(J2ee) 推荐的分层架构,尤其是Java Spring+Struct+Hibernate(SSH)组合的框架 将Dao层、 Service层,做了明确的定义和规范。由于SSH框架被广泛使用,其他框架,包括其他语言的框架(比如PHP)也都借鉴SSH,因此在这些框架里能看到Dao类、Service类的抽象定义。

服务层, Service Layer

在三层架构中,把业务逻辑层的上层逻辑分离出来,组成服务层。服务层往往是逻辑的表示层,即向上层(表示层)提供逻辑的外观。事务控制、安全检查、事务脚本等可以置入业务层(参考Martin fowler 的《企业应用的架构模式》)

四层架构总结

四层架构是三层架构的发展或进化。服务层的出现让三层架构的业务逻辑层不再变得臃肿。

四层架构和三层架构都存在一个不足,就是不强调前端的实现。当面对需要个性化定制界面、复杂用户交互、页面之间有依赖关系时,需要更好的解决方案。

MVC 模式

MVC 模式最主要包括三部分,Model View Controller(模型-视图-控制器)。

相比于三层架构或者四层架构,MVC最突出的优点是前端控制的灵活性。如果把MVC的View和Controller剥离出来,实际上是一种叫前端控制器模式的设计模式。

MVC 的缺点很明显,将前端以外逻辑都放到 Model里,随着业务增多,Model将越来越难以维护。

MVC 并不适合称作一种分层架构,更适合称为一种复合的设计模式。有人还将MVC模式归类为前端架构。

为什么MVC模式被广泛的采用? 笔者是这样认为的:

  • MVC模式最适合新闻门户网站、展示类网站,此类网站业务逻辑往往较为简单。
  • MVC模式最适合产品初创时被使用,因为项目初期逻辑简单, 使用MVC模式产品能快速成型,可以尽早投放市场进行试验(多数可能会被淘汰),这样就降低了试验成本。(如果产品有潜力,中后期随着业务增多和变得复杂,系统必然面临重构压力。)
  • MVC模式最适合产品原型的实现(注重前端)。

其他分层架构

除了三/四层架构,MVC模式,还有很多架构模式,但这些多是与三/四层架构、MVC模式类似,或是在他们之上进行扩展和改造。比如 MVVM (Model-View-ViewModel)、MVP、DDD架构模式等。

关于分层的总结

分层的思想或目的

  • 高内聚
  • 低耦合
  • 增强系统的可扩展性
  • 增强系统的可维护性

分层的优点

  • 开发人员可以只关注整个结构中的其中某一层;
  • 可以很容易的用新的实现来替换原有层次的实现;
  • 可以降低层与层之间的依赖;
  • 有利于标准化;
  • 利于各层逻辑的复用;
  • 结构更加的明确;
  • 在后期维护的时候,极大地降低了维护成本和维护时间

分层的缺点

相比于分层的优点,分层的缺点显得微不足道。

  • 使用分层虽然增多了代码量,但清晰的架构和代码复用却降低了开发成本、维护成本。
  • 分层虽然在逻辑上增多了代码的调用、增多了逻辑分支,降低了性能,但是这部分降低的性能与系统中真正的性能瓶颈(存储IO、网络IO、高层次的语言)相比,显得实在渺小。
  • 分层有时会导致级联的修改,但这种情况是可以通过面向接口的设计或者使用中介者模式、门面模式、适配器模式等设计模式解决。

如何设计分层架构

  • 软件的发展都是遵循着从简单到复杂的过程,软件架构也是一个迭代的过程,是一个循序渐进,不断完善的过程。简而言之,不断地重构。
  • 借鉴或者直接使用Java EE(j2me)的分层规范,并使用dao、service等类的命名。
  • 层与层之间交互部分,要遵循面向接口设计的原则,不吝舍使用设计模式。
  • 分层的选择要平衡成本和风险,使收益最大化。
  • 业务逻辑不要局限于四层架构或三层架构,依据领域业务特点可更细地划分层次。

什么是设计模式 – 总结

设计模式是如何诞生的?

  • 领域中有些东西是不变的,有些是不停变化的
  • 不变的东西和变化的东西之间,总存在着某些特定的关系,符合某种特定的规则和规律
  • 因为这些特定关系,经验丰富的设计者总在做重复的设计,也不断地复用自己的设计。他们为了设计的通用,也提出了非常好设计
  • 没有经验的设计者,却察觉不到这些关系,做不出好的设计
  • 经验丰富的设计者向没有经验的设计者传授好的、被重复使用的设计和经验,并讲述那些事物之间某些亘古不变的关系,这些就是设计模式的雏形
  • 将这些关系和设计进行整理和总结之后,设计模式诞生了。

设计模式是什么?

  • 是对程序设计人员经常遇到的设计问题的可再现的解决方案(The Smalltalk Companion)
  • 建立了一系列描述如何完成软件开发领域中特定任务的规则
  • 关注与复用可重复出现的结构设计方案
  • 提出了一个发生在特定设计环境中的可重复出现的设计问题,并提供解决方案
  • 识别并确定类和实例层次上或组件层次上的抽象关系

设计模式的作用?

  • 让开发人员能够控制系统的复杂度,不至于失控
  • 增强设计的可伸缩性/可扩展性(Scalable/scalability)
  • 增强设计的复用
  • 设计领域的通用语言(传递、沟通),设计模式具有很强的业务表述力

初学者掌握设计模式的方法

  • 模仿 – 最快的方法,学习和内化都比较迅速
  • 学习书本 – 比较而言学习较快,内化很慢
  • 不断重构自己的代码,自己去领悟设计模式 – 学习效率最慢,但是理解最透彻,内化效果最好的方法
  • 团队讨论、交流共享 – 学习较快、内化较快,但是时间成本很高,而且团队讨论往往不是高频率的活动

如何提升自己使用设计模式的能力

  • 学习设计模式的使用场景,简单地使用常用的设计模式
  • 改造设计模式,使之更好地满足业务场景
  • 总结业务场景,创建和组合新的设计模式
  • 将重复使用的设计模式及设计模式组合,创建框架。框架形成后,完善和丰富它。
  • 总之,把设计模式使用好,离不开经验,离不开积累
  • 在经验和积累之上,再创新。发现和创造新的模式、新的思想,并将这些新东西注入框架。

主流MVC框架的设计模式及遵守的软件设计原则

摘要

本文以主流的MVC框架为例,比如Java 的SSH、PHP的Symfony和Zend Framework ,在简单地剖析他们的设计原理之后,找到其中使用的设计模式;鉴赏他们的代码实现,查看设计者们都遵守了哪些软件设计原则。作此文,一为学习,二为总结。其中下面所写内容可能并不全面,也可能不准确,但会不断修改完善。

框架模式


MVC 模式

  • y = f (x)
  • View/Response = Controller (Model, Request)

Active Record 模式

  • Yii Active Record
  • Ruby on Rails Active Record

ORM 模式

  • PHP 的 Doctorine
  • Java 的 Hibernate

三/四层架构,3/4-tier Architecture

  • MVC 框架会借鉴三/四层架构的分层思想,对整个系统进行分层,最常见的是在 Model 层中再细分层次。
  • SSH 的 DAO/JavaBean、Service、Response 等
  • Symfony 2 的 Doctrine、Service、view 等

设计模式的使用


前端控制器,Frontend Controller Pattern

拦截过滤器模式,Intercepting Filter Pattern

复合模式,Compound Pattern

  • MVC框架如果不使用复合模式,本文也没有任何意义了

单例模式,Singleton Pattern

  • Controller 、 Context、 View 等框架核心类使用了单例模式

多例模式,Multition Pattern

  • 各类资源池、进程池、线程池(PHP 的后面两个用不上)

策略模式,Strategy Pattern

代理模式,Proxy Pattern

  • 抽象类的使用

工厂模式,Factory Methond Pattern

Action 对象的创建

抽象工厂模式,Abstract Factory Pattern

  • ajxaAction 、 WebAction 、 APIAction 以及 ajax和web组合的Action 或者 ajax和API组合的Action,复杂的框架会使用抽象工厂模式

门面模式,Facade Pattern

Logger 类,将第三方开源Logger类封装统一接口,将第三方 Logger 繁冗接口屏蔽

适配器模式,Adapter Pattern

  • Zend Framework 的 Zend_Db_Adapter 类,将具体不同的数据库(Mysql、Oracle、SQLite)类接口通过Driver类封装,为上层提供统一接口
  • Zend_Cache 对不同cache的接口的再次封装

模版模式,Template Method Pattern

  • 各种复杂逻辑的抽象类
  • 框架的扩展类

建造者模式,Builder Pattern

  • 依赖注入原则的遵守和实践,将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
  • Content 对象的创建者是 Controller
  • Config 对象的创建
  • ORM 的 Object 对象创建

桥梁模式,Bridge Pattern

命令模式,Command Pattern

  • Http 请求的解析就是命令模式
  • Symfony 2 的 Console
  • Symfony 2 的 Command 类

装饰模式,Decorator Pattern

  • ajxaAction 、 WebAction 、 APIAction 以及 ajax和web组合的Action 或者 ajax和API组合的Action

迭代器模式,Iterator Pattern

  • 路由设置的遍历

组合模式,Composite Pattern

观察者模式,Observer Pattern

  • 各种 Trigger 类
  • 各类自定义的扩展

责任链模式,Chain of Responsibility Pattern

  • 路由的解析,尤其是Restful风格的URL,依据路由表(责任链),定位Action 类和方法

访问者模式,Visitor Pattern

状态模式,State Patter

原型模式,Prototype Pattern

  • Action 中,讲执行权移交给另外一个 Action ,比如redirect 或者 forward,获得执行的Action还要和原有Action的属性、状态、上下文一致。原型模式是比较好的选择。

中介者模式,Mediator Pattern

  • 充当中介者的类是 Context,某些框架Context类,为Request、Respose、GlobalConfig 等等单例类进行抽象,为业务代码提供唯一的调用方式,即Context的类提供的接口

解释器模式,Interpreter Pattern

亨元模式,Flyweight Pattern

  • 通用状态类
  • 各种 Util 类
  • HTTP meta 类、特殊字符享元归类

备忘录模式,Memento Pattern

  • 系统日志、状态,以支持回滚
  • 数据库事务

软件设计原则 SOLID


单一职责原则(Simple responsibility pinciple SRP)

开-闭原则(Open-Closed Principle, OCP)

里氏代换原则(Liskov Substitution Principle,LSP)

迪米特法则(Law of Demeter LoD),最少知识原则(Least Knowledge Principle,LKP)

接口隔离原则(Interface Segregation Principle, ISP)

依赖倒置原则(Dependence Inversion Principle)

  • 建造者模式的使用
  • 路由表的使用,或其他各种配置都收集到配置文件中,使用依赖注入容器,实现代码和实现的解耦

Service Locator Patten 总结

核心思想

将构建依赖的接口彻底与依赖者分离,并将此依赖作为“服务”绑定到一个标识符,而后依赖者则可通过这个标识符获取被绑定的依赖。

优点

  • 上层逻辑不依赖于服务接口的具体实现,实现解耦
  • 提高系统的可扩展性
  • 逻辑分层更加简洁清晰,使不同层次的开发人员各司其职,提高团队开发效率

适用场景

与 (Abstract) Factory Patten 比较

  • Factory Patten 创建的所有结果对象多有统一的抽象接口的(更具一般性的)对象
  • Service Locator Patten 创建的服务,异构性程度比较大,接口缺少一般性
  • Factory Patten 单纯为创建对象
  • Service Locator Patten 职能更多些,包括创建服务、配置服务、注入服务的依赖等等

与 Dependence Injection Container 配合

  • Service Locator Patten 和 Dependence Injection并不互斥
  • Service Locator Patten 在定位服务时,创建服务、配置服务、注入服务的依赖通过Dependence Injection Container实现,可以进一步解耦

与 Proxy Patten 配合

  • 对创建的服务进行抽象,增强服务的一般性(即抽象出统一接口)

与 Singleton Patten 配合

  • 承担特定职责的Service Locator的对象无需多次实例化,节省开销
  • 对Service Locator定位的服务缓存做集中存储,存储数据结构存于Service Locator的单例对象即可

优化和扩展

  • 与多种设计模式配合使用
  • 在Service Locator类里定义服务实例化对象的缓冲池,使服务只实例化1次,节省开销

代码示例

symfony2 framework 使用总结

安装

The following exception is caused by a lack of memory and not having swap configured

  • 内存不足
  • 将PHP环境变量memory_limit调大,增大swap 交换内存

Compile Error: Cannot redeclare class Symfony\Component\Debug\Exception\FlattenException

  • 重复定义类名
  • ./vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Exception/FlattenException.php
  • ./vendor/symfony/symfony/src/Symfony/Component/Debug/Exception/FlattenException.php
  • 暂时不要使用Dubug功能了吧

php app/console doctrine:schema:update –force

配置symfony2\app\config\config.yml

mapping_types:
enum: string
set: string
varbinary: string
tinyblob: text

后台自动任务设计和编码总结

资源预估

  • 预估数据量、算法的时间、空间复杂度
  • 依据预估的结果分配合理的资源(内存,CPU),避免资源不够用。

避免数据脏读

自动脚本处理大量数据速度快,尤其有写操作的任务,数据一致性在分布式环境下往往难以保证。这种情况下应该避免数据脏读,比如数据库会有主从复制同步延迟的现象,这时应该强制连接数据库主库。

幂等性

有写操作的脚本,需要考虑脚本执行的幂等性,即在输入相同参数多次运行与运行一次的结果相同。 保证幂等性具体应该考虑一下几点:

  • 逻辑上,检查待处理数据的状态,已经处理过的数据不再处理。
  • 禁止并发运行,不允许运行多个进程同步运行同一任务。如何保证?信号量、Memcache加锁。

参数控制

  • 在设计阶段,应该通过参数控制,限制脚本的操作范围。参数可以是时间,自动脚本周期运行,每次运行,只处理相应周期范围内的数据。
  • 注意默认参数的参数值。

读并发

自动脚本处理速度快,在和其他系统进行交互时,会对其他系统产生大量而密集的请求。

  • 要考虑其他系统的抗并发能力
  • 合理限制单位时间对其他系统的访问次数
  • 尽量将数据集中做批量请求,减少请求次数
  • 在本地做缓存,消除重复的请求。

容错 和 错误处理

  • 容错 自动脚本往往批量处理大量数据,循环处理每条数据。在单次循环中的错误或异常,应该全部捕获,并记下日志,让脚本继续运行。
  • 日志 记录重要操作,包括info、warning、error等级所有日志,形式可以是文本记录或数据库,便于追查和报警。

备灾和数据修复

  • 在不可抗拒因素(断电,其他原因宕机)等引起任务中途突然停止,应该依据日志定位、和追查处理到哪一阶段。修复错误数据。
  • 在保证脚本幂等性的前提下,重复执行脚本可自动处理未完成的工作。

优化

随着业务增长,任务处理数据量越来越大,可能会出现,任务超时,内存不够用的情况,应该及时对自动任务进行优化,优化的方案可以从算法、业务架构上考虑。

Redis 和 Memcahe 比较和总结

Redis 和 Memcahe 比较和总结
项目 Redis Memcache
读速率 批量读效率高
写速率
冗余备份 master-slave模式,交换文件备份,支持binlog
内存使用率 依赖具体使用场景
主从复制 支持master – slaver 不支持 ,若需支持需要代理软件memagent
数据结构 key-value 、Set、 List 、Zset、hash key-value
数据的持久化 支持 不支持
特性 内存数据库、VM特性 内存缓存
存储方式 内存 + 交换文件备份 内存
未来发展 支持cluster

4种系统间交互方式比较

4种系统间交互方式比较

指相对独立子系统间的交互
指标\方式 API 数据文件 共享数据库 (web系统)根域名cookie
实效性 最高 实时
时间效率 最高
实时空间效率
实时占用带宽
系统设计正交性
系统设计耦合度
实现方式 同步/异步 异步 异步 异步
协议 http request,socket,… ftp,telnet,http,https,iSCSI,nfs… redis,memcache,mysql,MongoDB… http,https
数据结构 自定义 xml,yaml,csv,excel,txt,binany,… database
适用场景 时效性要求高,请求次数多,请求频率很高 时效性要求低,数据量小或中,请求频率最低 时效性要求最高,系统中,某几个对数据请求次数很高,请求频率最高 需要记录在浏览器中的信息
举例 单点登录中,cas服务器和cas客户端之间的交互 财务系统和银行的对账文件 分布式数据库,共享session,异步api,计费系统的数据库 单点登录系统中的登陆信息(ticket等)