#295 Python Supervisor 的简单配置

2019-03-11
name =
script-path-and-args =
execute-path =
python-path =
[program:<name>]
; process_name=<name>       ; 默认就是 program 名称

command = python <script-path-and-args>
directory = <execute-path>  ; 执行路径
environment=PYTHONPATH=<python-path>    ; 设置环境变量,逗号隔开
; user=

; killasgroup = false       ; 没用过
stopasgroup = true          ; 杀掉子进程,文档说是包含 killasgroup
; stopsignal=TERM           ; TERM, HUP, INT, QUIT, KILL, USR1, or USR2 中的一个
stopwaitsecs = 15

autostart = true
autorestart = true          ; true, false, unexpected
; exitcodes=0,2             ; 允许的退出码,否则会进入自动重启判断
; startretries = 3          ; 重启次数

numprocs=1
numprocs_start=0

loglevel = debug            ; critical, error, warn, info, debug, trace, blather
redirect_stderr = true
stdout_logfile = /var/log/<name>.log
stderr_logfile = /var/log/<name>.log

; stdout_logfile_maxbytes=1MB
; stdout_logfile_backups=10
; stdout_capture_maxbytes=1MB
; stdout_events_enabled=false

; stderr_logfile_maxbytes=1MB
; stderr_logfile_backups=10
; stderr_capture_maxbytes=1MB
; stderr_events_enabled=false

; 没用过
; umask=022
; priority=999
; serverurl=AUTO

另外,文档中看到一处有意思的用法,配置可以这样通过参数传递给进程:

[program:example]
command=/usr/bin/example --loglevel=%(ENV_LOGLEVEL)s
  1. 简单多进程就让 supervisor 控制
  2. 进程日志由进程自己控制,supervisor 只记录本身的运行与监控日志和进程漏出来的错误日志等。

#294 Bootstrap 基础模板

2019-03-04
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <title>HELLO WORLD</title>

    <link
      rel="stylesheet"
      href="/static/bootstrap@3.4.1/css/bootstrap.min.css"
      integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu"
      crossorigin="anonymous"
    />
  </head>

  <body>
    <div class="container">
      <h1>你好,世界!</h1>
    </div>

    <script
      src="/static/jquery@1.12.4/dist/jquery.min.js"
      integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
      crossorigin="anonymous"
    ></script>
    <script
      src="/static/bootstrap@3.4.1/js/bootstrap.min.js"
      integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
      crossorigin="anonymous"
    ></script>
  </body>
</html>
  1. 样式参考:https://v3.bootcss.com/getting-started/#examples
  2. 相关文件:
  3. http://code.jquery.com/jquery-1.12.4.min.js
  4. https://github.com/twbs/bootstrap/releases/download/v3.4.1/bootstrap-3.4.1-dist.zip

#292 关于缓存

2019-02-12

什么地方需要缓存?

  • DB 查询,加上缓存可以减小数据库压力,同时提升性能。
  • 某些费时费资源的操作,加上缓存可以避免重复计算。

缓存方式

  1. 本地缓存
  2. 进程内缓存(内存)
  3. 磁盘缓存
  4. 缓存服务(redis/memcache)
  5. 数据库:有些任务可以提前进行计算,将结果存在数据库中。

本地缓存的问题是多个节点之间容易出现数据不一致的情况。我听说过 Java 的一些本地缓存组件,应该其中有一些可以做到多个节点之间的数据同步。如果要是自己实现的话,可以在服务中增加一个刷新缓存的接口调用,其中一个节点刷新缓存时,调用其他节点的刷新接口。也可以引入 MQ,避免这个调用造成的耦合和可能的性能损耗。

缓存策略

  1. 提前进行一些计算,将内容缓存起来。如有必要,可以选择合适的时间间隔进行数据刷新。
  2. DB 缓存可以由数据库中间层来做,也可以有客户端库来做,或者就在应用的数据库层中实现。
    查询时,先尝试本地缓存,再尝试缓存服务(两道缓存,避免击穿),最后再进行数据库查询。
    缓存的数据应该是这样的:
  3. 高命中(缓存命中率需要做好监控)
  4. 较少变更
  5. 尽可能保证数据变更之后(不一致问题)不会产生严重影响
    如果一致性要求很高的话,要反复确实是否必须使用缓存,如果确定的话,缓存刷新策略需要考虑清楚。

穿透

数据库没有数据,缓存也没有数据。这样的请求直接穿过缓存读数据库,给数据库照成压力。

  1. 一般是非法请求所致,对于这一部分请求应该有机制可以进行过滤掉
    恶意请求,或者高频请求
  2. 可以对没有数据的请求也进行缓存,可以给一个相对小一点的 TTL
  3. 布隆过滤器 参考:2021/03/07, 布隆过滤器
  4. 将空 Key 单独存到一个 Redis SET 中应该也可以
    比如 system:cache:empty_keys:hhmm, TTL = 123,每次用 SISMEMBER 查询当前分钟和上一分钟

击穿

某个热 Key 失效,导致大量请求打到数据库。
热 Key 应该预热,然后有一个比较大的 TTL,甚至没有过期时间。最后,通过定时刷新任务来更新这些热 Key。

雪崩

大量 Key 同时过期, 导致请求直接打到数据库,然后影响整个系统。
大面积的击穿。

  1. 比如系统刚启动的时候,批量写入大量数据,这些数据有相同的 TTL,就会同时过期。我们应该给过期时间加入一些随机,将过期时间点分散在一个区间内。
  2. 对于热 Key 的处理, 同击穿部分。

其他:

  1. 为了防止 Redis 奔溃,导致系统崩溃,应该在本地进程中也设置一个缓存。
    LocalCache -> RedisCache -> DB
  2. 为了防止数据库奔溃,数据库请求应该由一个队列来处理。
  3. 网关部分对于大量来不及处理的请求应该丢弃。
  4. 缓存应该能够按重要性划分一下级别,如果遇到问题能够快速丢弃不重要的数据
    还应该可以快速丢弃指定服务的所有缓存。
    这应该叫做缓存降级。

预热

根据之前的经验,或者开发者预判,将部分数据事先写入缓存。
如果提供相关工具,让系统维护人员能够方便快捷地管理缓存数据,能够手动介入缓存的生命周期就更好了。至少在后台提供一个 缓存预热 的按钮。

  1. 提供指定热 Key,让定时任务负责刷新。
    智能预热:给访问量大的 Key 延长 TTL, 启动定时刷新。
  2. 需要对缓存的访问有一个简单的监控,方便作为之后预热的依据。

缓存更新

另起一篇:缓存更新策略

#291 RuntimeError: Event loop is closed

2019-02-11

开发过程中发现报错:

Traceback (most recent call last):
  ...
  File "/usr/local/python3.7.3/lib/python3.7/asyncio/streams.py", line 353, in close
    return self._transport.close()
  File "/usr/local/python3.7.3/lib/python3.7/asyncio/selector_events.py", line 690, in close
    self._loop.call_soon(self._call_connection_lost, None)
  File "/usr/local/python3.7.3/lib/python3.7/asyncio/base_events.py", line 719, in call_soon
    self._check_closed()
  File "/usr/local/python3.7.3/lib/python3.7/asyncio/base_events.py", line 508, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

经过检查,发现是我在代码中调用了 asyncio.run(协程方法(*args, **kwargs))
总是有些同步代码中需要调用协程方法,没有办法。

改成这样就可以了:

asyncio.get_event_loop().run_until_complete(协程方法(*args, **kwargs))

如果是在线程中就改成:

asyncio.new_event_loop().run_until_complete(协程方法(*args, **kwargs))

否则会报:RuntimeError: There is no current event loop in thread 'Thread-2'.

线程中 run_sync

RuntimeError: This event loop is already running

attached to a different loop

Traceback (most recent call last):
  File "/opt/fuckTheWorld/core/framework.py", line 194, in fuck
    value = asyncio.new_event_loop().run_until_complete(lego.get(key))
  File "/usr/local/python3.7.3/lib/python3.7/asyncio/base_events.py", line 616, in run_until_complete
    return future.result()
  File "/opt/fuckTheWorld/core/lego.py", line 30, in execute
    data = await super().execute(*args, **options)
  File "/opt/fuckTheWorld/lego/client.py", line 476, in execute
    return await conn.retry.call_with_retry(
  File "/opt/fuckTheWorld/lego/retry.py", line 51, in call_with_retry
    return await do()
  File "/opt/fuckTheWorld/lego/client.py", line 455, in _send_command_parse_response
    return await self.parse_response(conn, command_name, **options)
  File "/opt/fuckTheWorld/lego/client.py", line 494, in parse_response
    response = await connection.read_response()
  File "/opt/fuckTheWorld/lego/connection.py", line 934, in read_response
    await self.disconnect()
  File "/opt/fuckTheWorld/lego/connection.py", line 822, in disconnect
    await self._writer.wait_closed()  # type: ignore[union-attr]
  File "/usr/local/python3.7.3/lib/python3.7/asyncio/streams.py", line 359, in wait_closed
    await self._protocol._get_close_waiter(self)
RuntimeError: Task <Task pending name='Task-45' coro=<Fuck.execute() running at /opt/fuckTheWorld/core/lego.py:30> cb=[_run_until_complete_cb() at /usr/local/python3.7.3/lib/python3.7/asyncio/base_events.py:184]> got Future <Future pending> attached to a different loop

#290 Python 解析 UserAgent

2019-02-08

GitHub 上搜索到的这些项目:

ua-parser 和 user-agents 这两个库数据漂亮些,所以,就选这两个库研究研究。

ua-parser

from ua_parser import user_agent_parser

ua_string = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.104 Safari/537.36'

parsed_string = user_agent_parser.Parse(ua_string)
pp.pprint(parsed_string)

user-agents


#289 Nginx: multi_accept

2019-02-05

http://nginx.org/en/docs/ngx_core_module.html#multi_accept

Syntax: multi_accept on | off;
Default: multi_accept off;
Context: events

If multi_accept is disabled, a worker process will accept one new connection at a time. Otherwise, a worker process will accept all new connections at a time.

The directive is ignored if kqueue connection processing method is used, because it reports the number of new connections waiting to be accepted.

翻译

如果 multi_accept 禁用, worker 进程一次只接受一个连接
如果启用, 则会一次性接受所有连接。
如果连接处理方法为 kqueue, 则该指令被忽略, 因为 kqueue 会报告新连接的数量(然后好分配)。

PS: 关于连接处理方法, 参考 2017/02/07, Nginx 连接处理方法

理解

感觉有点迷糊, 那么究竟 multi_accept 的作用是什么呢?

Nginx 采用一个 master 进程和多个 worker 进程的模式工作。多个 worker 共享一个 socket (端口绑定), 当请求进来的时候, 被调度到的进程就会去 accept 连接。
multi_accept 的作用就是控制他一次拿走一个连接, 还是拿走所有等待中的连接。

这个参数可以让调度更加高效。如果请求数一直维持在一个很高的水平, 可以设置为 on。
但是在请求数不大的场景下, 那可能会导致同一时刻, 多个进程之间的负载就会不那么均衡, 总是 1C 干活, 7C 围观。

#288 MySQL 分区

2019-02-01
partition_options:
    PARTITION BY
        { [LINEAR] HASH(expr)
        | [LINEAR] KEY [ALGORITHM={1 | 2}] (column_list)
        | RANGE{(expr) | COLUMNS(column_list)}
        | LIST{(expr) | COLUMNS(column_list)} }
    [PARTITIONS num]
    [SUBPARTITION BY
        { [LINEAR] HASH(expr)
        | [LINEAR] KEY [ALGORITHM={1 | 2}] (column_list) }
      [SUBPARTITIONS num]
    ]
    [(partition_definition [, partition_definition] ...)]

partition_definition:
    PARTITION partition_name
        [VALUES
            {LESS THAN {(expr | value_list) | MAXVALUE}
            |
            IN (value_list)}]
        [[STORAGE] ENGINE [=] engine_name]
        [COMMENT [=] 'string' ]
        [DATA DIRECTORY [=] 'data_dir']
        [INDEX DIRECTORY [=] 'index_dir']
        [MAX_ROWS [=] max_number_of_rows]
        [MIN_ROWS [=] min_number_of_rows]
        [TABLESPACE [=] tablespace_name]
        [(subpartition_definition [, subpartition_definition] ...)]

subpartition_definition:
    SUBPARTITION logical_name
        [[STORAGE] ENGINE [=] engine_name]
        [COMMENT [=] 'string' ]
        [DATA DIRECTORY [=] 'data_dir']
        [INDEX DIRECTORY [=] 'index_dir']
        [MAX_ROWS [=] max_number_of_rows]
        [MIN_ROWS [=] min_number_of_rows]
        [TABLESPACE [=] tablespace_name]

分区类型

  • [LINEAR] HASH(expr) 根据值的哈希分区
  • [LINEAR] KEY [ALGORITHM={1 | 2}] (column_list)
  • RANGE{(expr) | COLUMNS(column_list)} 根据值得范围分区
  • LIST{(expr) | COLUMNS(column_list)} 根据不同的值分区

COLUMNS 不限于整数

创建分区

PARTITION BY LIST(column) (
    PARTITION a VALUES IN (a1, a2, a3),
    PARTITION b VALUES IN (b1, b2, b3),
    PARTITION c VALUES IN (c1, c2, c3)
)

PARTITION BY RANGE(column) (
    PARTITION 2012q1 VALUES LESS THAN('2012-04-01'),
    PARTITION 2012q2 VALUES LESS THAN('2012-07-01'),
    PARTITION 2012q3 VALUES LESS THAN('2012-10-01'),
    PARTITION 2012q4 VALUES LESS THAN('2013-01-01')
)

PARTITION BY HASH(column) PARTITIONS 128
PARTITION BY HASH(dayofmonth(date)) PARTITIONS 31

查看分区信息

SELECT * FROM `information_schema`.`PARTITIONS`;

子分区

  1. PARTITION 关键字换成 SUBPARTITIONPARTITIONS 关键字换成 SUBPARTITIONS,接在分区语句后面。
  2. 可以是不同类型。

比如:

PARTITION BY HASH (prod_id) SUBPARTITION BY HASH (cust_id)
PARTITIONS 4 SUBPARTITIONS 4;

脚本

如果是 By Range 分区,一般需要自动创建新的分区,删除久的分区。

比如:

CREATE TABLE `test` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `date` DATE NOT NULL,
    `key` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_general_ci',
    `value` VARCHAR(300) NOT NULL COLLATE 'utf8mb4_general_ci',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`, `date`) USING BTREE,
    UNIQUE INDEX `key` (`date`, `key`) USING BTREE
)
COLLATE='utf8mb4_general_ci'
/*!50100 PARTITION BY RANGE (to_days(`date`))
(PARTITION p20230123 VALUES LESS THAN (738909) ENGINE = InnoDB,
 PARTITION p20230124 VALUES LESS THAN (738910) ENGINE = InnoDB,
 PARTITION p20230125 VALUES LESS THAN (738911) ENGINE = InnoDB)  */;

然后,通过下面这个 cron 任务自动更新分区:

#!/bin/bash

# 开启调试模式,输出每条执行的命令及其执行结果
set -x

# 检查当前机器 IP 地址中是否包含指定的 VIP(虚拟 IP)
# 确认在主 MySQL 上执行
vip_w="192.168.12.34"
if [ $(/sbin/ip a | grep "${vip_w}" | wc -l) -eq 0 ]; then echo 'WARN: Wrong Machine!!!'; exit 1; fi

# 删除 90 天前的分区
# PS:如果分区不存在,TRUNCATE 不会报错。
delete_date=$(date -d '90 days ago' +%Y%m%d)
mysql -uroot -p123456 -e "USE test; ALTER TABLE test TRUNCATE PARTITION p$delete_date;"  # DROP

# 创建未来分区
create_date=$(date -d '7 days' +%Y%m%d)
mysql -uroot -p123456 -e "USE test; ALTER TABLE test ADD PARTITION (PARTITION p$delete_date VALUES LESS THAN (TO_DAYS("$delete_date")));"

#287 PuTTY 配色

2019-01-30

参考:https://github.com/altercation/solarized/blob/master/putty-colors-solarized/solarized_dark.reg

Colour21\255,255,255\
Colour20\187,187,187\
Colour19\85,255,255\
Colour18\0,187,187\
Colour17\255,85,255\
Colour16\187,0,187\
Colour15\85,85,255\
Colour14\0,0,187\
Colour13\255,255,85\
Colour12\187,187,0\
Colour11\85,255,85\
Colour10\0,187,0\
Colour9\255,85,85\
Colour8\187,0,0\
Colour7\85,85,85\
Colour6\0,0,0\
Colour5\0,255,0\
Colour4\0,0,0\
Colour3\85,85,85\
Colour2\0,0,0\
Colour1\255,255,255\
Colour0\187,187,187\
Colour0\131,148,150\
Colour1\147,161,161\
Colour2\0,43,54\
Colour3\7,54,66\
Colour4\0,43,54\
Colour5\238,232,213\
Colour6\7,54,66\
Colour7\0,43,56\
Colour8\220,50,47\
Colour9\203,75,22\
Colour10\133,153,0\
Colour11\88,110,117\
Colour12\181,137,0\
Colour13\101,123,131\
Colour14\38,139,210\
Colour15\131,148,150\
Colour16\211,54,130\
Colour17\108,113,196\
Colour18\42,161,152\
Colour19\147,161,161\
Colour20\238,232,213\
Colour21\253,246,227\

#286 电子邮件的一些小技巧

2019-01-14

紧急程度 X-Priority

有另一个 Priority 头,不知道为什么没有使用。

  • 1:最高
  • 2:高级
  • 3:一般
  • 4:低级
  • 5:最低

根据网上的一些资料,这几个头已经被垃圾邮件滥用,导致可能会被拦截。
不过,企业邮箱使用应该没有问题。

网易邮箱发送的时候勾选重要,就会设置 X-Priority: 1

需要发送回执 Disposition-Notification-To

效果就是客户端会提示:“是否发送回执”,确认 or 取消。
注意,不是所有客户端会支持回执。

mdn-request-header = "Disposition-Notification-To" ":" 1#mailbox
# mailbox 就是 local-poart@domain 这种格式
# RFC 中的这个 1# 我也不知道是干嘛用的

值是正常的邮件地址格式就行了,比如:

Disposition-Notification-To: admin@example.com

Disposition-Notification-To: =?utf-8?b?566h55CG5ZGY?= admin@example.com

注意:会和 Return-Path 的域名部分做对比,如果不一致,不会发送回执。

参考:https://www.rfc-editor.org/rfc/rfc2298.html

回执参考

主要是有这么一段:

Content-Type: message/disposition-notification; name="MDNPart2.txt"
Content-Disposition: inline
Content-Transfer-Encoding: 7bit

Reporting-UA: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Thunderbird/91.7.0
Final-Recipient: rfc822;huang@example.com
Original-Message-ID: <2ec11bc0.59a.1803f61e0f9.Coremail.admin@example.com>
Disposition: manual-action/MDN-sent-manually; displayed

参考资料与拓展阅读