flickr 的全局主键生成方案

flickr 的全局主键生成方案

类似于大航海的的数据库设计,我们的用户分库有 voyage_1/2/3/4 … 那么uid怎样生成?

现在的做法是在用一张索引表 voyage_share.user_index 取其自增主键,insert_id 便是uid。但缺点是,有单点负载的风险。

flickr提供了一个扩展的更好的方案: 他们把 user_index 抽出一个专门用作生成 uid 的表,例如取名叫 uid_sequence,并拆成若干的字表,自增步长设置为2(机器数目),这两张表可以放在不同的物理机器上。 其中一个表负责生成奇数uid,另一个负责生成偶数uid

uid_sequence 表的设计

比如创建64位的自增id:

CREATE TABLE `uid_sequence` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `stub` char(1) NOT NULL default '',
  PRIMARY KEY  (`id`),
  UNIQUE KEY `stub` (`stub`)
) ENGINE=MyISAM;

SELECT * from uid_sequence 输出:

+-------------------+------+
| id                | stub |
+-------------------+------+
| 72157623227190423 |    a |

如果我需要一个全局的唯一的64位uid,则执行:

REPLACE INTO uid_sequence (stub) VALUES ('a');
SELECT LAST_INSERT_ID();
  • 用 REPLACE INTO 代替 INSERT INTO 的好处是避免表行数太大,还要另外定期清理。
  • stub 字段要设为唯一索引,这个 sequence 表只有一条纪录,但也可以同时为多张表生成全局主键,例如 user_ship_id。除非你需要表的主键是连续的,那么就另建一个 user_ship_id_sequence 表。
  • 经过实际对比测试,使用 MyISAM 比 Innodb 有更高的性能。

这里flickr使用两台数据库作为自增序列生成,通过这两台机器做主备和负载均衡。

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1

TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2

MySQL 中 last_insert_id() 的并发问题

因为是两条SQL语句,所以这两条语句之间会不会有并发问题?

答案是不会,因为 last_insert_id() 是 Connection 级别的,是单个连接客户端里执行的insert语句最近一条,客户端之间是不会影响,没必要锁定和事务处理。

Memcache 内存分配策略

Page 是内存分配的最小单位

Memcached的内存分配以page为单位,默认情况下一个page是1M,可以通过-I参数在启动时指定。

如果需要申请内存时,memcached会划分出一个新的page并分配给需要的slab区域。page一旦被分配在重启前不会被回收或者重新分配(page ressign已经从1.2.8版移除了)

Chunk 是存放缓存数据的单位

启动时有一个成长因子,默认是1.25,按容量大小递增,所以会产生碎片空闲

Slab 是一堆相同大小 Chunk 的容器

Memcached在启动时通过 -m 指定最大使用内存,但不会启动就占用,是动态按需分配。

slab申请内存时以page为单位,所以在放入第一个数据,无论大小为多少,都会有1M大小的page被分配给该slab。申请到page后,slab会将这个page的内存按chunk的大小进行切分,这样就变成了一个chunk的数组,在从这个chunk数组中选择一个用于存储数据。

Slab/Page/Chunk 之间的关系

为什么总内存没占满,还会触发LRU机制?

我的理解是,当全部内存已经被slab以page方式申请了,化为了chunk,内存容器全划分好后,不会回收。 例如:100字节的chunk所在slab中因为key过期释放了空间,但200字节的chunk所在的slab是满的。所以总和上看,内存没有占满。

现在有一个200字节的数据要存入,就会发现存不进200字节的chunk,此时就会触发LRU机制自动清理200字节chunk中的数据。即使100字节chunk所在slab还有不少内存空闲。

PHP-FPM 的进程数动态和静态

PHP-FPM 的进程数动态和静态

  • pm = static
  • pm.max_children:静态方式下开启的php-fpm进程数量
  • pm.start_servers:动态方式下的起始php-fpm进程数量
  • pm.min_spare_servers:动态方式下的最小php-fpm进程数量
  • pm.max_spare_servers:动态方式下的最大php-fpm进程数量

1. 如果 pm=static,那么其实只有pm.max_children这个参数生效

系统会开启设置数量的php-fpm进程。

2. 如果 pm=dynamic,那么pm.max_children参数失效,后面3个参数生效。

系统会在php-fpm运行开始的时候启动pm.start_servers个php-fpm进程,然后根据系统的需求动态在pm.min_spare_servers和pm.max_spare_servers之间调整php-fpm进程数。

如何选择

事实上,跟Apache一样,运行的PHP程序在执行完成后,或多或少会有内存泄露的问题。这也是为什么开始的时候一个php-fpm进程只占用3M左右内存,运行一段时间后就会上升到20-30M的原因了。

  • 内存紧张的机器或VPS,用动态的。
  • 内存充裕的,完全可以用静态的,因为这样不需要进行额外的进程数目控制,会提高效率。因为频繁开关php-fpm进程也会有时滞。

并发量的估算

pm.max_children = 32G (机器物理内存) / 64MB (每个进程的 memory_limit) = 256

假设一个PHP请求耗费250ms,那一个进程1秒就可以处理4次请求

那一台32G的机器可支撑的QPS = 4 * pm.max_children = 2000~3000

带宽的预估

并发请求数 * 5K (单次请求耗费流量,单位字节) / 1000 * 8 = 带宽 (单位比特)

参考

(总结)Nginx使用的php-fpm的两种进程管理方式及优化

MySQL 配置文件 my.cnf 的读入顺序

首先查看当前运行 mysql 进程时有没有指定 my.cnf 文件:

ps aux | grep mysql | grep 'my.cnf'

如果上面的命令没有输出,表示启动 mysql 时没有指定 --defaults-file=/path/to/my.cnf

敲入以下命令,得知 my.cnf 的默认搜寻顺序:

mysql --help | grep 'Default options' -A 1

会显示如下信息:

Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/local/etc/my.cnf ~/.my.cnf

我们依次到以上目录去找是否有对应的 my.cnf。

如果启动 mysql 既没有设置 --defaults-file,默认的读取目录也找不到 my.cnf 文件,那么表示 mysql 启动时没有任何加载配置文件,而是使用默认配置。

MacOS 里通过 homebrew 安装的 MySQL 5.7 缺省启动时就没有加载 my.cnf

敲入 locate my*.cnf 可列出所有 my.cnf 文件:

/usr/local/Cellar/mysql/5.7.14/support-files/my-default.cnf
/usr/local/Cellar/mysql/5.7.14/mysql-test/include/default_my.cnf
/usr/local/Cellar/mysql/5.7.14/mysql-test/suite/federated/my.cnf
/usr/local/Cellar/mysql/5.7.14/mysql-test/suite/ndb/my.cnf
.....

第一个 support-files/my-default.cnf 是我们要找的,把它复制到 /usr/local/etc/ 下。

cp /usr/local/opt/mysql/support-files/my-default.cnf /usr/local/etc/my.cnf

重启 mysql 后配置即可生效。

sudo launchctl unload -w /Library/LaunchDaemons/homebrew.mxcl.mysql.plist
或
launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist

PHP 静态延迟绑定

简单复习一下 PHP 静态延迟绑定

发现有时候一些知识长久不用就容易生锈,比如“静态延迟绑定”,这个名词现在听上去竟有些陌生,其实这个特性在我们项目早已大量使用了 原理相当于就是在子类继承父类时,会把父类中的 self 关键字全文替换为父类的类名,实现绑定。

所以当我们在子类中调用 self::xxx 时,其实调用的还是父类的成员,要想调用子类自己的,需要使用 static::xxx 关键字

摘抄一段大航海项目中的代码做示例:

class Model_Npc_Abstract extends Core_Model_Abstract
{
    public function __construct($npcId)
    {
        if (! $npc = static::read($npcId)) {
            throws(_('指定NPC不存在。') . 'NpcId:' . $npcId);
        }

        $this->_prop  = $npc;
    }
}

class Model_Npc_Regular extends Model_Npc_Abstract
{
    /**
     * 加载 NPC 详情
     *
     * @param $npcId
     * @return array
     */
    public static function read($npcId)
    {
        if (! $npc = Dao('Static_NpcRegular')->get($npcId)) {
            return array();
        }

        return $npc;
    }
}

父类 Model_Npc_Abstract 中如果写的是 self::read() 则会提示找不到 read 方法

使用一款语法高亮插件 highlight.js

这款语法高亮插件叫做 highlight.js 使用非常简单,支持多种主题,其中有我们非常亲切的 monokai_sublime

我们现就来试试吧

class Model_MainTask_Abstract extends Core_Model_Abstract
{
    public function __construct($taskId)
    {
        if ($taskId < 1) {
            throws403('Invalid MainTaskId');
        }

        if (! $task = Dao('Static_MainTask')->get($taskId)) {
            throws(_('主线任务静态配置缺失。') . 'MainTaskId:' . $taskId);
        }

        $this->_prop = $task;
    }
}

很酷吧,使用方法如下:

1、先引入JS并初始化

<link rel="stylesheet" href="/res/js/highlight/styles/monokai_sublime.css" />
<script src="/res/js/highlight/highlight.pack.js"></script>
<script>hljs.initHighlightingOnLoad();</script>

2、这样括起代码

或者这样括起:

<pre><code class="html">...</code></pre>