先说八字流派

八字流派特别多。

不管是事业,还是婚姻感情,看法也不尽相同。我听说过的就有以下几种:

  • 格局:八字用神,专求月令;也有看月令透干的流派。
  • 盲派:铁口直断,根据四柱八字形象、干支组合等断大事件,出口往往戳中痛点。
  • 旺衰:以日干旺衰为核心,追求五行平衡,旺了,克泄;衰了,帮扶。主要分《滴天髓》派和新派。滴天髓派多体现在断语上,而新派更多倾向于公式化(一个土二个木各种占比计算之类)。
  • 禄命:听过,但没有见过。以年命为主看纳音和神煞。

这几种流派我们不讨论孰优孰劣,涉及的东西太多,了解甚浅,不好评价。但我觉得,格局很好用。

成大格者成大事。

格局看事业——张敬尧

《明通赋》:向官旺以成功,入格局而致贵。如何看事业,用一篇文章只能讲一丢丢丢丢丢。估计要肝个一百篇才能讲个七七八八。

想来想去,还是找个案例合适些。

张敬尧(1881-09-18 ~ 1933-05-07),字勋臣,安徽省颍州府霍丘县人,保定军官学校毕业。中华民国及满洲国军事将领,因与日本合作被国民政府军事调查统计局(军统)刺杀身亡。

《穷通宝鉴·八月乙木》有此命例:秋木盛,煞高有制,贵为都督,早年午运最佳,人后运不得地,故起伏不常,辰运癸酉年,被剌殒命。

张敬尧生于光绪七年闰七月廿五日卯时,八字如下:

辛丁乙己
巳酉卯卯 乾

丙申、乙未、甲午、癸巳、壬辰,五岁起运。

乙木,生于农历八月,七杀格。日时卯木禄神,旺。大抵伤官七杀,最喜身旺,有制伏为妙。

七杀也称偏官。经曰:人有偏官,如抱虎而眠,虽借其威足以慑群畜,稍失关防,不应其噬脐,不可不虑也。

丁火食神制月令酉金,七杀、食神旺,一看就很溜。辛坐巳,丁坐酉,长生之地。

时辰己土,喜还是不喜?偏财,喜。年支巳火伤官,忌。

喜忌分析完,一目了然。午运(1910 ~ 1914 年,30 ~ 34 岁)美滋滋,乙长生于午,丁禄于午。这五年应该是他人生最舒坦的。

1912 年,团长;壬子,合丁冲午,吉。

1913 年,旅长;癸丑,巳酉丑合七杀金局,不吉。

1914 年,师长;甲寅,合己,吉。

癸运(1915 ~ 1919 年,35 ~ 39 岁),癸,此处当偏印看,吉。

1916 年,总司令;丙辰,合辛、酉,透丙,不吉。

1918 年,省长;戊午,财食,吉。任湖南督军兼省长,纵兵殃民,甘心媚外。

1919 年,己未,吉。五四运动时,湖南人民开展驱张运动。

巳运(1920 ~ 1924 年,40 ~ 44 岁),巳,伤官透,不吉。

1920 年,被免职;庚申,合乙合巳,不吉。此年北洋政府被迫将其去职。

1922 年,壬戌,吉。奉系败北,张敬尧投奔直系吴佩孚。

壬辰运(1925 ~ 1934 年,45 ~ 54 岁),丁壬合,辰酉合,杀动,皆不吉。

1925 年,军长;乙丑,七杀得局,不吉。张敬尧投鲁军的张宗昌,被任命为直鲁联军第 2 军军长。

1926 年,副司令;丙寅,不吉。

1928 年,戊辰,合酉,不吉。张宗昌败北,张敬尧随之下野,逃往大连。

1932 年,投靠伪满政权。壬申,合丁,不正,吉。

1933 年,总司令;癸酉,酉为七杀白虎,都是凶神,不吉。5 月 7 日,在北平东交民巷六国饭店,被刺客袭击而身亡。

辛巳、丁酉,天干丁克辛,地支半合——辛是国家政府(日本、伪满政权)。表面上我弄死你,背地里却跟你相好,这不就是汉奸的象嘛。

壬运,合去食神,七杀生威。

酉聚白虎,也死于白虎。

林庚白先生批道(《人鉴·命理存验》):

乙木阴柔,虽秋生,得两禄不弱,惟辛金七杀太旺,喜丁食制之。

三十岁后,午运长生,又丁火食神见禄,必佳运。

癸丁相战,有己土制癸,反仇为恩,故屡立战功,位至湘督。

庚申流年,交入巳运,巳初有金,岁运皆杀,宜其不振,辛酉尤劣。

明年四十五,交入壬运,丁壬化木,化凶而吉,八月后必有动机。

丙寅丁卯,四十六七,又握重柄,戊辰已巳两年亦佳。

庚午交辰初乙木,五十、五十一两年,财权两旺。

壬申五十二,急流勇退,否则癸酉一年有凶。

“癸酉一年有凶”。

或许,这就是命吧。命该如此,半点不由人。

最后更新时间 2021-07-04.

本文整理自2ccc盒子iny的帖子 → 链接到原贴
破解 → 链接

一、绿色版介绍

  • 绿色版为完全版,没做任何的精简,也没有做任何的改动!
  • 绿色版不含任何破解,仅供研究,24 小时后请自行删除,如需继续使用 RAD Studio,请购买正版。

二、绿色版文件包说明

绿色版共包含 4 个文件包,分别为:

  • [必选] dotnetfx45_full_x86_x64.exe:Microsoft .NET Framework 4.5
  • [必选] RAD Studio 10.4.2 绿色版.7z:基础包
  • [必选] RAD Studio 10.4 Green 1.4.rar:绿化程序
  • [可选] RAD Studio 10.4.2 PlatformSDKs.7z:Android SDK、Android NDK

三、绿色版使用说明

  1. 安装 Microsoft .NET Framework 4.5,只安装一次即可,Windows 10 已自带,可以跳过此步;
  2. 解压 RAD Studio 10.4.2 绿色版.7z 到任意目录;
  3. 解压 RAD Studio 10.4.2 PlatformSDKs.7z 解压到绿色版的Extra目录 (10.4\Extra\CatalogRepository\);
  4. 解压 RAD Studio 10.4 Green 1.4.rar 解压到绿色版目录下 (10.4\RADStudioSydneyGreen.exe);
  5. 运行 RADStudioSydneyGreen.exe 可实现绿化和卸载。

四、下载地址

1. Microsoft .NET Framework 4.5 (Windows 10 已自带,可以跳过此步)

官方下载地址:http://download.microsoft.com/download/b/a/4/ba4a7e71-2906-4b2d-a0e1-80cf16844f5f/dotnetfx45_full_x86_x64.exe

2. RAD Studio 10.4.2 绿色版.7z (3.69GB,解压后 26.2GB)

下载地址:https://pan.baidu.com/s/1xmMoFKEt--Kud7_6lB7Dgg
提取码:je3q
MD5: 7C078BC1C78DE33A70F76D98A193E8F6
SHA1: 37F43F18E57FA6B5C8245169C2D024A26B047998

3. RAD Studio 10.4.2 PlatformSDKs.7z (2.31GB,解压后 5.91GB)

下载地址:https://pan.baidu.com/s/1TdS9B2t4ymcwrRQwqZKBiw
提取码:40w3
MD5: 34DD02D1EDE2BF62580C76F424E72C2F
SHA1: ED3C5D4E35DAB4E8B8ED07582317A042734719E2

4. RAD Studio 10.4 Green 1.4.rar(965KB)

下载地址:https://pan.baidu.com/s/1_YYv2QiQRJRXolH1u80bsg
提取码:0tod
MD5: 837FF4733D85FF4F7C77B803804AEC32
SHA1: 5F81CFE8BB39A4F983A63C64050ACCAFE86193AF

附:RAD Studio 10.4 Green Update History

-1.4

  • 支持RAD Studio 10.4.2

-1.3

  • 支持RAD Studio 10.4.1

-1.2

  • 修正部分环境变量路径错误

-1.1

  • 修正关联文件路径错误
  • 修正帮助文件路径错误
  • 修正卸载会连同 10.3 一起卸载的错误

-1.0

  • RAD Studio 10.4 绿化程序第一版

传统部署的坑:

1202 年了,如果你连 Docker 都不知道是什么,我建议买一本书看看——或者谷歌一下,博客已经写烂了。

为什么有这篇文章,是因为我在真正做容器化改造的时候,发现公司生产环境存在大量的坑:

  • 传统虚拟机部署,基本依赖克隆或者手工编译。由于人力原因,SRE 历来单传,编译出来的 PHP、扩展等二进制版本不一致;
  • 项目开发人员痛苦不堪——他没办法模拟出接近于线上一致的环境(碰不到摸不着,各种扩展版本都要自己去编译);
  • 新人入职都会灵魂拷问你一句——我怎么把线上的代码跑起来?
  • ……

用什么 Linux 发行版?

Ubuntu 应该是全球用户量最多的发行版了,嗯我说的是桌面这一块,折腾过的人都知道,出问题的时候开机会有 “检测到系统错误” 的提示,另外,网上提供的配置或者各种疑难杂症,改了不一定能生效,而且你还不确定改了会不会影响到别的,反正我是不敢用的哈哈(Manjaro 真香)。

CentOS 应该是大家最熟悉的,也是我见过最多应用在生产环境中的。它给我的感觉就是非常稳定,并且网上的资料是一搜索就展现在你面前,而你对着资料改配置,重载就生效,不会搞出什么问题。

公司的生产环境清一色 CentOS 6,但 CentOS 6 已经被官方弃用,不再提供 yum 镜像源,这也意味着很多包你都安装不了,所以你只能升级到 CentOS 7。

问题来了,我能升级吗?

这不得不说到之前线上出现过一个故障:

公司有一台发布构建机器,用来做代码部署,机器上安装了 NodeJS、Go 编译器等,有一天前端的同事说向 SRE 同学提了一个需求:

升级 NodeJS 到 v10 版本,因为以前的 v6 版本太旧了,SRE 同学也没多想,发现 CentOS 6 机器要升级 glibc 才行,于是运维的同事就升级 glibc 之后,升级了 NodeJS;

过了段时间有人部署某服务,该服务使用了 结巴分词 ,部署完发现线上挂了……

嗯,线上环境的 glibc 版本比较低,编译机的 glibc 版本高,部署过去不兼容直接就是启动不了,还好当时回滚的够快 :)

直接用 7 也不是不可以,统一就 OK,但要命的是,发现有些祖传的 PHP 扩展,已经失传了,能兼容但是你怎么保证不出问题对不对?

经历万般挫折,最终使用的是 CentOS 6.9,好在腾讯云有 yum 源,东拼西凑了生产环境的 PHP 扩展之后,开发环境已经完美投入使用。

就是因为这些事情,前前后后花了两三周的时间都在折腾镜像。

小而美 VS 大而全:

CentOS 是真的大!我自己也使用 7 重新打了一个镜像,发现不管怎么清理各种缓存,最终的镜像大小都接近 1G!

虽然说也不是不能用,但我就是有洁癖呀。最后还是选择了 alpine ,把体积减少到 100M 以内。

到这里可能有人问:我们生产环境用的 alpine 也就 60M 左右,没有那么大吧?

之前看过这个项目 Laradock ,它的特点是定制化非常强,基本都是打开一些环境变量就可以构建出你所要的镜像;

但我更倾向于,牺牲一些磁盘空间,制作一个统一的环境。为了方便,线上没必要按照项目复制扩展,维护自己的 Dockerfile,统一都放进去就好了,维护起来也比较方便。

生产环境使用什么版本?

公司目前大量使用 PHP 5.4 和 PHP 7.2,扩展版本比较混乱;

没有直接使用 nginx,而是使用 openresty 1.11.2(主要是传统 IDC 部署缺乏云上 WAF ,需要自行做好限流和 IP 防刷);

我提供的 Dockerfile 是 PHP 5.6 和 PHP 7.2 的最新版本,理论上可以直接升级;而 openresty 使用最新奇数版本,保证生产环境的稳定和安全。

一些细节(坑)

记录一下为什么要花这么长的时间整这个镜像,个人觉得下面列举出来的,都是非常宝贵的经验:

镜像:

  • 尽量合并 RUN 指令,减少镜像层数,从而缩小镜像体积;

apk:

  • 官方的镜像非常慢,所以使用了阿里云的镜像加速;
  • apk --no-cache 的使用,也可以缩小镜像体积,对于自己安装的扩展不要忘记 rm 掉没用的文件夹;
  • composer 安装私有仓库依赖 git 命令,所以它需要被安装;
  • git clone 私有仓库需要 ssh-key,我的实现方式是 base64 编码文件内容,再 echo 到对应的位置上去,这样的好处就是一个 Dockerfile 就可以到处走了,不需要额外的文件和 COPY 指令,既方便又减少层数!
  • 通过 apk 安装下来的扩展,需要手工 cp 到 /usr/local/lib/php/extensions/no-debug-non-zts-20131226/ 目录下;

文件权限:

  • 私钥的文件权限是 600,只有文件的拥有者具有读写权限,组里其他用户或者其他用户连读都不行,不这样做的话代码拉不下来(ssh 会报错),切记;

线上排障:

  • bind-tools 的作用在于方便线上定位问题——有时候你不得不进去容器,发现没办法测试 DNS 解析,你会特别痛苦;

环境标准化:

  • 统一应用目录 /www 和日志目录 /wwwlog;

文件权限:

  • 用户和用户组的 id,此处是 500(CentOS 6),CentOS 7 是 1000——如果你使用 NFS 共享文件系统,需要统一 www 的 uid,不然文件权限问题会令你抓狂;
  • 公司使用 www 用户,官方提供的 fpm 镜像自带 www-data 用户,我代码重度洁癖,所以就把它删了;
  • 定时任务建议使用 www 用户运行,原因是日志目录有可能是被运维的同事挂在到宿主机采集(一台宿主机一个 filebeat 进程,节省资源),而你使用 root 用户创建的某些文件夹,其他人可能写不进去,但还是留了后手——给 root 设置密码,遇到问题说不定可以 su 解决;

扩展:

  • 公司重度使用 RabbitMQ 消息队列组件,所以安装了 amqp 扩展,rabbitmq-c-dev 等基础包必须加上,不然没办法编译通过;
  • redis、bcmath、gettext、pdo_mysql、mysqli、mbstring、gd、zip、opcache 这几个扩展几乎都是必装的,其他的像 yaf、sysvmsg 等不需要的,大家可以自行删除;

php-fpm.conf:

  • 非常驻模式启动,容器才不会刚启动就退出了;
  • 修改子进程数量,还有超时等配置,这部分与线上环境是一致的;

php.ini:

  • 打开 cli 模式的 opcache 扩展,加速 PHP 的运行,主要是一些定时任务;
  • 关闭 PHP 的版本输出,这样别人访问我的网站就不知道我使用哪个 PHP 版本了,安全无小事!

适用于生产环境的 PHP 5 Dockerfile:

FROM php:5.6.40-fpm-alpine3.8

LABEL maintainer="??? <???@???.com>"

ENV TZ=Asia/Shanghai

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    # deps
    apk --no-cache add bind-tools \
        git \
        make \
        openssh-client \
        php5-mcrypt \
        php5-sysvmsg \
        php5-sysvsem \
        php5-sysvshm \
        tzdata \
        freetype-dev \
        gettext-dev \
        imagemagick-dev \
        libmemcached-dev \
        libpng-dev \
        libzip-dev \
        jpeg-dev \
        rabbitmq-c-dev \
        && \
    cp /usr/lib/php5/modules/mcrypt.so  /usr/local/lib/php/extensions/no-debug-non-zts-20131226/ && \
    cp /usr/lib/php5/modules/sysvmsg.so /usr/local/lib/php/extensions/no-debug-non-zts-20131226/ && \
    cp /usr/lib/php5/modules/sysvsem.so /usr/local/lib/php/extensions/no-debug-non-zts-20131226/ && \
    cp /usr/lib/php5/modules/sysvshm.so /usr/local/lib/php/extensions/no-debug-non-zts-20131226/ && \
    # DNS
    [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf && \
    # timezone
    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
    echo '$TZ' > /etc/timezone && \
    # /www
    # /wwwlog
    # /app
    addgroup -g 500 -S www && \
    adduser -u 500 -D -S -G www www && \
    mkdir /www && mkdir /wwwlog && mkdir -p /app && \
    chown -R www:www /www && chown -R www:www /wwwlog && chown -R www:www /app && \
    addgroup www tty && \
    sed -i 's/\/home\/www:\/bin\/false/\/home\/www:\/bin\/ash/g' /etc/passwd && \
    deluser --remove-home www-data && \
    # password
    passwd root -d "!!!Production!!!" && \
    passwd www  -d "!!!Production!!!" && \
    # ssh-key
    mkdir -p /root/.ssh && \
    echo ???==|base64 -d>/root/.gitconfig && \
    echo ???==|base64 -d>/root/.ssh/config && \
    echo ???==|base64 -d>/root/.ssh/id_rsa && \
    echo ???==|base64 -d>/root/.ssh/id_rsa.pub && \
    echo ???==|base64 -d>/root/.ssh/known_hosts && \
    chmod 600 /root/.ssh/id_rsa && \
    # composer
    wget -O /usr/local/bin/composer https://mirrors.cloud.tencent.com/composer/composer.phar && \
    chmod +x /usr/local/bin/composer && \
    /usr/local/bin/composer config -g repos.packagist composer https://mirrors.cloud.tencent.com/composer/ && \
    # ext
    docker-php-ext-configure zip --with-libzip && \
    docker-php-ext-configure gd --with-jpeg-dir=/usr/lib --with-freetype-dir=/usr/include/freetype2 && \
    pecl install -o -f amqp-1.10.2 && \
    pecl install -o -f memcached-2.2.0 && \
    pecl install -o -f imagick-3.4.4 && \
    pecl install -o -f rar-4.2.0 && \
    pecl install -o -f redis-4.3.0 && \
    pecl download yaf-2.3.5 && tar zxvf yaf-2.3.5.tgz && cd yaf-2.3.5 && phpize && ./configure && \
    make && make install && cd .. && rm -rf yaf-2.3.5 && \
    docker-php-ext-install bcmath gettext mysqli pcntl sockets pdo_mysql mysqli mbstring gd zip opcache && \
    docker-php-ext-enable amqp mcrypt memcached imagick rar redis sysvmsg sysvsem sysvshm yaf && \
    rm -rf /tmp/pear /var/cache/apk/* /tmp/* && \
    # php-fpm.conf
    echo "[global]"        > /usr/local/etc/php-fpm.d/zz-docker.conf && \
    echo "daemonize = no" >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
    # www.conf
    rm -f /usr/local/etc/php-fpm.d/www.conf.default && \
    sed -i "s/www-data/www/g"                                                  /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/pm.max_children = 5/pm.max_children = 128/g"                     /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/listen = 127.0.0.1:9000/listen = 127.0.0.1:9056/g"               /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;pm.max_requests = 500/pm.max_requests = 1024/g"                 /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;request_slowlog_timeout = 0/request_slowlog_timeout = 5/g"      /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;request_terminate_timeout = 0/request_terminate_timeout = 30/g" /usr/local/etc/php-fpm.d/www.conf && \
    sed -i 's/;slowlog = log\/\$pool.log.slow/slowlog = \/proc\/self\/fd\/2/g' /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;access.format/access.format/g"                                  /usr/local/etc/php-fpm.d/www.conf && \
    # php.ini
    cp /usr/local/etc/php/php.ini-production                 /usr/local/etc/php/php.ini && \
    sed -i "s/;opcache.enable_cli=0/opcache.enable_cli=1/g"  /usr/local/etc/php/php.ini && \
    sed -i "s/expose_php = On/expose_php = Off/g"            /usr/local/etc/php/php.ini

WORKDIR /app

适用于生产环境的 PHP 7 Dockerfile:

FROM php:7.2.34-fpm-alpine3.12

LABEL maintainer="??? <???@???.com>"

ENV TZ=Asia/Shanghai

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    # deps
    apk update && \
    apk --no-cache add bind-tools \
        git \
        make \
        openssh-client \
        tzdata \
        freetype-dev \
        libmemcached-dev \
        libpng-dev \
        jpeg-dev \
        && \
    # DNS
    [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf && \
    # timezone
    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
    echo '$TZ' > /etc/timezone && \
    # /www
    # /wwwlog
    # /app
    addgroup -g 500 -S www && \
    adduser -u 500 -D -S -G www www && \
    mkdir /www && mkdir /wwwlog && mkdir -p /app && \
    chown -R www:www /www && chown -R www:www /wwwlog && chown -R www:www /app && \
    addgroup www tty && \
    sed -i 's/\/home\/www:\/sbin\/nologin/\/home\/www:\/bin\/ash/g' /etc/passwd && \
    deluser --remove-home www-data && \
    # password
    passwd -u root -d "!!!Production!!!" && \
    passwd -u www  -d "!!!Production!!!" && \
    # ssh-key
    mkdir -p /root/.ssh && \
    echo ???==|base64 -d>/root/.gitconfig && \
    echo ???==|base64 -d>/root/.ssh/config && \
    echo ???==|base64 -d>/root/.ssh/id_rsa && \
    echo ???==|base64 -d>/root/.ssh/id_rsa.pub && \
    echo ???==|base64 -d>/root/.ssh/known_hosts && \
    chmod 600 /root/.ssh/id_rsa && \
    # composer
    wget -O /usr/local/bin/composer https://mirrors.cloud.tencent.com/composer/composer.phar && \
    chmod +x /usr/local/bin/composer && \
    /usr/local/bin/composer config -g repos.packagist composer https://mirrors.cloud.tencent.com/composer/ && \
    # ext
    docker-php-ext-configure gd --with-jpeg-dir=/usr/lib --with-freetype-dir=/usr/include/freetype2 && \
    pecl install -o -f memcached-3.1.5 && \
    pecl install -o -f redis-5.3.4 && \
    docker-php-ext-install bcmath pdo_mysql gd opcache && \
    docker-php-ext-enable memcached redis && \
    rm -rf /tmp/pear /var/cache/apk/* /tmp/* && \
    # php-fpm.conf
    echo "[global]"        > /usr/local/etc/php-fpm.d/zz-docker.conf && \
    echo "daemonize = no" >> /usr/local/etc/php-fpm.d/zz-docker.conf && \
    # www.conf
    rm -f /usr/local/etc/php-fpm.d/www.conf.default && \
    sed -i "s/www-data/www/g"                                                  /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/pm.max_children = 5/pm.max_children = 256/g"                     /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/listen = 127.0.0.1:9000/listen = 127.0.0.1:9072/g"               /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;pm.max_requests = 500/pm.max_requests = 1000/g"                 /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;request_slowlog_timeout = 0/request_slowlog_timeout = 5/g"      /usr/local/etc/php-fpm.d/www.conf && \
    sed -i "s/;request_terminate_timeout = 0/request_terminate_timeout = 30/g" /usr/local/etc/php-fpm.d/www.conf && \
    sed -i 's/;slowlog = log\/\$pool.log.slow/slowlog = \/proc\/self\/fd\/2/g' /usr/local/etc/php-fpm.d/www.conf && \
    # php.ini
    cp /usr/local/etc/php/php.ini-production                 /usr/local/etc/php/php.ini && \
    sed -i "s/;opcache.enable_cli=0/opcache.enable_cli=1/g"  /usr/local/etc/php/php.ini && \
    sed -i "s/expose_php = On/expose_php = Off/g"            /usr/local/etc/php/php.ini

WORKDIR /app

适用于生产环境的 openresty Dockerfile:

FROM openresty/openresty:1.19.3.2-alpine-apk

LABEL maintainer="??? <???@???.com>"

ENV TZ=Asia/Shanghai

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    # deps
    apk update && \
    apk --no-cache add bind-tools \
        tzdata \
        && \
    # DNS
    [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf && \
    # timezone
    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \
    echo '$TZ' > /etc/timezone && \
    # /www
    # /wwwlog
    # /app
    addgroup -g 500 -S www && \
    adduser -u 500 -D -S -G www www && \
    mkdir /www && mkdir /wwwlog && mkdir -p /app && \
    chown -R www:www /www && chown -R www:www /wwwlog && chown -R www:www /app && \
    addgroup www tty && \
    sed -i 's/\/home\/www:\/sbin\/nologin/\/home\/www:\/bin\/ash/g' /etc/passwd && \
    # password
    passwd -u root -d "!!!Production!!!" && \
    passwd -u www  -d "!!!Production!!!"

COPY conf/nginx.conf            /usr/local/openresty/nginx/conf/nginx.conf
pcre_jit             on;
user                 www www;
worker_processes     1;
error_log            /usr/local/openresty/nginx/logs/error.log error;
worker_rlimit_nofile 65535;

events {
    use                epoll;
    worker_connections 65535;
}

http {
    include      mime.types;
    default_type application/octet-stream;

    server_tokens off;
    sendfile      on;
    tcp_nopush    on;
    tcp_nodelay   on;

    server_names_hash_bucket_size 512;
    client_max_body_size          8m;
    client_header_buffer_size     32k;
    large_client_header_buffers   4 32k;

    proxy_buffers           32 128k;
    proxy_buffer_size       128k;
    proxy_busy_buffers_size 128k;

    client_body_timeout   10;
    client_header_timeout 10;
    send_timeout          30;
    keepalive_timeout     60;

    log_format main escape=json '{"@timestamp":"$time_iso8601",'
                                 '"scheme":"$scheme",'
                                 '"remote_host":"$host",'
                                 '"clientip":"$remote_addr",'
                                 '"bytes":$body_bytes_sent,'
                                 '"cost":$request_time,'
                                 '"referer":"$http_referer",'
                                 '"agent":"$http_user_agent",'
                                 '"time_local":"$time_local",'
                                 '"xforward":"$http_x_forwarded_for",'
                                 '"method":"$request_method",'
                                 '"request":"$request_uri",'
                                 '"uri":"$uri",'
                                 '"postData":"$request_body",'
                                 '"cookieData":"$http_cookie",'
                                 '"httpversion":"$server_protocol",'
                                 '"reqid":"$reqid",'
                                 '"remote_port":"$remote_port",'
                                 '"server_port":"$server_port",'
                                 '"status":$status}';

    fastcgi_connect_timeout      300;
    fastcgi_send_timeout         300;
    fastcgi_read_timeout         300;
    fastcgi_buffer_size          64k;
    fastcgi_buffers              4 64k;
    fastcgi_busy_buffers_size    128k;
    fastcgi_temp_file_write_size 256k;
    fastcgi_intercept_errors     on;

    gzip              on;
    gzip_vary         on;
    gzip_comp_level   5;
    gzip_buffers      16 8k;
    gzip_min_length   1k;
    gzip_proxied      any;
    gzip_http_version 1.0;
    gzip_disable      "msie6";
    gzip_proxied      expired no-cache no-store private auth;
    gzip_types        text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/xml+rss application/json;

    server {
        listen      80 default_server;
        server_name _;
        return      444;
        access_log  /usr/local/openresty/nginx/logs/access.log main;
        include     add_header_reqid.conf;
    }

    include /usr/local/openresty/nginx/conf/vhost/*.conf;
}

嗯,真实的生产环境配置十分混乱,我做了格式化,大家拿去用吧 :)

阅读本文大约需要 2.9 分钟。

曾经的我是一名艺术生,如今是一名程序员。

聊起画画,相信很多人都有疑惑:同样都是画人物,为什么我画不像,或者说我根本就不知道怎么下手。

你不是一个人。给你们看看江苏考卷:

“我明明在画人,但看着好像在画鬼。”


实际上,要把一个人画得像他本人,是需要下苦功夫的。

作为一个过来人,我觉得有两方面:

第一,眼高才能手高,多剖析物体的造型,多看多学习优秀作品。

人类对这个世界的认知是由浅入深的。在我看来,画画和写代码是有很大共同点的。

在计算机的世界里,软件开发离不开“抽象”二字。

抽象,就是在一个具体的问题中,提取出共性的东西,以此来解决复杂的问题。

比如抽象出一个“登录”接口,毕竟在现实世界中,有很多很多的登录方式——QQ、华为、小米、微信……

既然是抽象,那它就是可以解决同类问题的。

很多网站的登录,都有QQ、微信登录,那么 A 写的代码 B 就可以直接拿来用了,这就是一种抽象!

工作这么多年,第一次发现原来 Ctrl+C 和 Ctrl+V 可以被讲得这么高大上。

画画也是同样的道理。

我不知道读这篇文章的人有没有接触过“素描几何体”:

“我不想画几何体,我只想学习怎么画人。”

我真的听过很多这样的话。

再看看下面几张图:

是不是越来越有感觉了?

素描几何体,也只是对世界万物的抽象罢了。

鼻头、鼻翼——球状物体;

鼻梁——梯形、长方体……

如果鼻子比较高,鼻头就是个大球、圆润;

如果鼻子比较塌,鼻球就比较平,那就把球体削掉一点……

眼睛呢?叫眼球吧,秒懂:

至于嘴唇,上唇中间的部位,叫做唇珠,可以简单理解为球体:

更细致一些,它还有顶面和底面,顶面夹起来又延伸到鼻子下方,形成人中……

新手画人真的太难了!

看看水杯,就一个圆柱体嘛,手柄会难画一些,可以分段拆着画。

音箱,就是一个大大的长方体,音箱上的喇叭是圆形的,正中间还有个小球体。

当然,我也不是说一定要从几何体开始,我见过也有部分人跳过这一步,直接走了第二步,天天画日日画,最终画得也不错。

没错,第二点,就是熟能生巧。

站在岸上学不会游泳。不说画人,没有基础的人,画个最简单的长方体都会画错。

为什么?除了透视问题(这个属于认知范畴),还有就是他不知道怎么下手,一根线都画不直。

脑:我知道怎么画了。

手:我是谁我在哪儿我不知道。

这个没办法,只能多多练习,而且时间长了,肌肉记忆会丧失。

可以看看于小冬是怎么画人物的,滑到最底下有视频~

快速勾勒出大动态,确定好头、身体、四肢的位置,

再深入刻画人物细节,

而不是一上来就陷入细节当中。

一切以大局为重!!!

新手画人基本都是,

我先画头,画眉毛,一二三四五六七八根,

画睫毛,一二三四五六七八九十根,画长一点,比较美,

画眼线,画眼角,画眼珠,

画鼻孔,

画嘴,

画腮红……

哦豁,化妆和画画?道理是一样的。

有些人颧骨高,想把颧骨往里收,妆该怎么化?

颧骨高且突出,就人为地把这个转折点往里面画一画,并弱化原来位置。

鼻梁塌,那就把鼻梁两侧的转折加强些。

多画,多化~

积累到一定程度,逐渐就是肌肉记忆了。

眼到手到,一气呵成。


阅读本文大约需要 40 分钟。

上一篇文章 《大白话讲讲 Go 语言的 sync.Map(一)》 讲到 entry 数据结构,原因是 Go 语言标准库的 map 不是线程安全的,通过加一层抽象回避这个问题。

当一个 key 被删除的时候,比如李四销户了,以前要撕掉小账本,现在可以在大账本上写 expunged,

对,什么也不写也是 OK 的。也就是说,

entry.p 可能是真正的数据的地址,也可能是 nil,也可能是 expunged。

为什么无端端搞这个 expunged 干嘛?因为 sync.Map 实际上是有两个小账本,

一个叫 readOnly map(只读账本),一个叫 dirty map(可读、也可写账本):

type Map struct {
    mu sync.Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

既然有账本一个变成两个,那肯定会有些时候出现两个 map 数据是不一致的情况。

readOnly 结构的 amended 字段,是一个标记,为 true 的时候代表 dirty map 包含了一些 key,这些 key 不会存在 readOnly map 中。

这个字段的作用,在于加速查找的过程。

假设 readOnly 账本上有 张三、李四、钱五,dirty 账本除了这三个人,后面又新增了 王六,查找逻辑就是这样的:

  1. 先在 readOnly 查找,王六不在
  2. 判断 amended ,发现两个账本数据是不一致的
  3. 再去 dirty 账本查找,终于找到王六

如果 2 的 amended 标记是两个账本数据一致,那就没有执行 3 的必要了。

我们可以看看源码是怎么实现的:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
  read, _ := m.read.Load().(readOnly)
  // 1. 先在 readOnly 查找,王六不在
  e, ok := read.m[key]
  // 2. 判断 amended ,发现两个账本数据是不一致的
  if !ok && read.amended {
    // 加锁的原因是,前面步骤 1 的读取有可能被另一个协程的 missLocked 更改了
    // 导致读出来的值不符合预期,所以加锁再读取一次,老套路了。
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    e, ok = read.m[key]
    if !ok && read.amended {
      // 3. 再去 dirty 账本查找,终于找到王六
      e, ok = m.dirty[key]
      // missLocked 拥有一个计数器,
      // 它的作用在于 readOnly 如果一直查不到,经常退化到 dirty,
      // 那就把 dirty 作为 readOnly ,直接取代它。
      m.missLocked()
    }
    m.mu.Unlock()
  }
  if !ok {
    return nil, false
  }
  return e.load() // 还记得大账本吗?这里是拿到最终的值,针对 entry.p == expunged 做了特殊处理。
}

func (m *Map) missLocked() {
  // 查不到就递增 misses 计数器
  m.misses++
  // 这个判断条件不是常数,而是 dirty map 的记录数。
  // 这个判断条件很奇妙,
  // 它使得 dirty 取代 readOnly 的时机,和 dirty 的数据量正相关了。
  // 也就是说,dirty map 越大,对两个 map 不一致的容忍度越大,
  // 不会有频繁的取代操作。
  if m.misses < len(m.dirty) {
    // 如果不是经常查不到,说明 readOnly 还是可以用的,退出。
    return
  }
  // 如果 readOnly 已经没有存在价值,那就把 dirty 取代 readOnly。
  // 此时,dirty 置空,并把 misses 计数器置 0。
  // read 和 dirty 的数据类型都是 map[interface{}]*entry,
  // 可以直接替换,无需类型转换,这个设计简直完美。
  m.read.Store(readOnly{m: m.dirty})
  m.dirty = nil
  m.misses = 0
}

func (e *entry) load() (value interface{}, ok bool) {
  p := atomic.LoadPointer(&e.p)
  // entry.p 可能是真正的数据的地址,也可能是 nil,也可能是 expunged
  if p == nil || p == expunged {
    // nil 或者是 expunged 都是不存在的,返回空
    return nil, false
  }
  // 如果是真正的数据地址,那就返回真正的数据(就是拿到大账本的某一页纸上的内容)
  return *(*interface{})(p), true
}

到这里已经讲完数据读取这部分的代码了,接着再讲数据是怎么写入的。

上一篇文章我留了一个思考题,

为什么小账本不能做到同时修改?限于篇幅,我不会展开。

我现在解答我们有了大账本,是如何做到同时修改的!

答案在这里:

// tryStore 顾名思义,就是不断尝试的意思。
// 你可以看到有一个无条件的死循环,只有某些条件满足的时候才会退出
// 计算机术语:自旋(自己一直在旋转)
func (e *entry) tryStore(i *interface{}) bool {
  for {
    p := atomic.LoadPointer(&e.p)
    // readOnly map 存储的是 entry 结构,p 就是所谓的大账本,
    // p 指向大账本上某一页纸上的内容,
    // 当账本查不到的时候,返回查不到。
    if p == expunged {
      return false
    }
    // 当账本可以查到的时候,使用 CAS 把旧的值,替换为新的值。
    // 可以查到并替换成功,返回成功,函数退出
    // 查不到或者替换失败,自旋,重试,直到成功为止
    if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
      return true
    }
  }
}

问题来了, CAS(Compare and Swap,比较并交换)是什么东西?我们看这个加法函数:

func add(delta int32) {
  for {
    // 把原先的值取出来
    oldValue := atomic.LoadInt32(&addr)
    // 读取后,如果没有其他人对它修改(Compare)
    // 那就用 oldValue+delta 新值,替换掉原来的值(Swap)
    // 成功程序退出,失败了就自旋重试(可能被其他人改了导致 Compare 不成功)
    if atomic.CompareAndSwapInt32(&addr, oldValue, oldValue+delta) {
      return
    }
  }
}

越来越有趣了,atomic.CompareAndSwapInt32 到底是个啥子哟?

它的具体实现在 src/runtime/internal/atomic/asm_amd64.s 里(不同 CPU 架构,使用的文件不同,这里以最常见的 amd64 为例):

// bool Cas(int32 *val, int32 old, int32 new)
// Atomically:
//  if (*val == old) {
//    *val = new;
//    return 1;
//  } else
//    return 0;
TEXT runtime∕internal∕atomic·Cas(SB),NOSPLIT,$0-17
  MOVQ  ptr+0(FP), BX
  MOVL  old+8(FP), AX
  MOVL  new+12(FP), CX
  LOCK
  CMPXCHGL  CX, 0(BX)
  SETEQ  ret+16(FP)
  RET

FP(Frame pointer: arguments and locals):

函数的输入参数,格式 symbol+offset(FP),symbol 没有实际意义,只为了增强代码可读性,但没有 symbol 程序无法编译。

ptr+0(FP) 代表第一个参数,取出复制给 BX 寄存器。

由于 ptr 是一个指针,在 64 位的处理器中,一个指针的占 8 个字节,

所以第二个参数 old+8(FP),偏移量 offset 等于 8,

而第三个参数 new+12(FP),偏移量再加 4 的原因是 int32 占据 4 个字节。

LOCK 指令前缀会设置处理器的 LOCK# 信号,锁定总线,阻止其他处理器接管总线访问内存,

设置 LOCK# 信号能保证某个处理器对共享内存的独占使用。

CMPXCHGL CX, 0(BX) 是比较并交换的指令,将 AX 和 CX 比较,相同将 BX 指向的内容 放入 CX,

CMPXCHGL 暗中使用了 AX 寄存器。

兜了一大圈,终于明白大账本的数据是怎样被更新的了。

看看数据是怎么写入之前,我们要知道数据是怎么被删除的:

// 删除的逻辑是比较简单的。
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
  read, _ := m.read.Load().(readOnly)
  e, ok := read.m[key]
  // key 不存在的时候并且 readOnly map 和 dirty map 不一致时,
  // 把 dirty map 对应的记录删了。
  if !ok && read.amended {
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    e, ok = read.m[key]
    if !ok && read.amended {
      // 数据不一致的时候,最终读出来的值以 dirty map 为主,
      // 即使 readOnly map 是 !ok 的,但 dirty map 可能是 ok 的,
      // 既然值可能是存在的,那就读取出来。
      e, ok = m.dirty[key]
      // 删除操作
      delete(m.dirty, key)
      // 递增数据不一致的计数器。
      // 太多不一致会把 dirty map 提升为 readOnly map,前面讲过了。
      m.missLocked()
    }
    m.mu.Unlock()
  }
  // key 存在的时候,把 key 置为 nil,注意这里不是 expunged,
  // 这也是我为什么要先讲 Delete 的原因。
  if ok {
    return e.delete()
  }
  return nil, false
}

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
  m.LoadAndDelete(key)
}

// delete 将对应的 key 置为 nil!而不是 expunged!
func (e *entry) delete() (value interface{}, ok bool) {
  for {
    p := atomic.LoadPointer(&e.p)
    if p == nil || p == expunged {
      return nil, false
    }
    if atomic.CompareAndSwapPointer(&e.p, p, nil) {
      return *(*interface{})(p), true
    }
  }
}

OK,我们看数据写入的逻辑,它是整个源码中最难理解的,隐含的逻辑关系非常多:

// unexpungeLocked 将 expunged 的标记变成 nil。
func (e *entry) unexpungeLocked() (wasExpunged bool) {
  return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// storeLocked 将 entry.p 指向具体的值
func (e *entry) storeLocked(i *interface{}) {
  atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
// tryExpungeLocked 尝试 entry.p == nil 的 entry 标记为删除(expunged)
func (e *entry) tryExpungeLocked() (isExpunged bool) {
  p := atomic.LoadPointer(&e.p)
  // for 循环的作用,可以保证 p != nil,
  // 保证写时复制过程中,p == nil 的情况不会被写到 dirty map 中。
  for p == nil {
    if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
      return true
    }
    p = atomic.LoadPointer(&e.p)
  }
  return p == expunged
}

// dirtyLocked 写时复制,两个 map 都找不到新增的 key 的时候调用的。
func (m *Map) dirtyLocked() {
  // dirty 被置为 nil 的情景还记得吗?
  // 
  // 当 readOnly map 一直读不到,需要退化到 dirty map 读取的时候,
  // dirty map 会被提升为 readOnly map,
  // 此时,dirty map 就会被置空。
  //
  // 但是,dirtyLocked 被调用之前,
  // 都是判断 read.amended 是否为 false
  // if !read.amended {...}
  // 个人认为,可以直接判断 if m.dirty == nil {...},
  // 代码可读性更强!下面三行代码也可以不要了。
  if m.dirty != nil {
    return
  }
  // 遍历 readOnly map,把里面的内容都复制到新创建的 dirty map 中。
  read, _ := m.read.Load().(readOnly)
  m.dirty = make(map[interface{}]*entry, len(read.m))
  for k, e := range read.m {
    // tryExpungeLocked 将 entry.p == nil 设置为 expunged,
    // 遍历之后,所有的 nil 都变成 expunged 了。
    // 返回 false 说明 p 是有值的,要拷贝到 dirty 里。
    // Delete 操作会把有值的状态,转移为 nil,
    // 并不会把 expunged 状态转移为 nil,
    // 由于 for 循环的存在,p 也不会等于 nil,
    // 也就是说,tryExpungeLocked 的 p == expunged 是可以信任的。
    if !e.tryExpungeLocked() {
      // 如果没有被删除,拷贝到 dirty map 中。
      m.dirty[k] = e
    }
  }
}

func (m *Map) Store(key, value interface{}) {
  // 如果 readOnly map 有对应的 key,
  // 通过 e.tryStore 直接写入(就是上面更新大账本的整个过程),
  // 注意,tryStore 会在 entry.p == expunged 的情况下失败。
  read, _ := m.read.Load().(readOnly)
  if e, ok := read.m[key]; ok && e.tryStore(&value) {
    return
  }
  // readOnly map 找不到,或者 key 被删除了,
  // 那就写到 dirty map 里面。
  m.mu.Lock()
  read, _ = m.read.Load().(readOnly)
  if e, ok := read.m[key]; ok {
    // unexpungeLocked 将 expunged 的标记变成 nil。
    // 当 entry.p == expunged,并且成功替换为 nil,
    // 返回 true。
    // 
    // 这个分支的意义在于,写时复制 dirtyLocked 的时候,
    // 数据从 readOnly map 搬迁到 dirty map 中,
    // 如果 p 是被删除的,dirty 是不会有这个 key 的,
    // 所以要把它也写进 dirty 中,保证数据的一致性。
    // 
    // 为什么好端端的 expunged,要改成 nil?
    // unexpungeLocked 是一个原子操作,成功的话,
    // 说明 p == expunged,
    // 说明写时复制已经完成。
    // 
    // 为什么要写时复制完成之后,才可以去改 dirty?
    // 我理解是这样的:
    // 如果不这样做,dirty 会被你修改成 Store 传进来的参数,
    // 写时复制又把它修改成 readOnly map 的值,
    // 所以更新 readOnly map 就好了。
    // 
    // 这一块的细节真的非常多,每一块地方都要小心处理好。
    if e.unexpungeLocked() {
      m.dirty[key] = e
    }
    // 写入值。
    e.storeLocked(&value)
  } else if e, ok := m.dirty[key]; ok {
    // 如果 dirty map 存在就直接更新进去,这个很好理解,
    // 因为 readOnly map 找不到会来 dirty 查。
    e.storeLocked(&value)
  } else {
    // 两个 map 都找不到的时候,说明这是一个新的 key。
    // 
    // 1. 如果 dirty 之前被提升为 readOnly,那就导一份没有被删除的 key 进来。
    // 
    // 这个判断条件,我理解等价于 if m.dirty == nil {...}
    if !read.amended {
      // 初始化 m.dirty,并把值写进去(写时复制)
      m.dirtyLocked()
      // amended 设置为不一致。
      // amended 表示 dirty 是否包含了 readOnly 没有的记录,
      // 很明显,read.m[key] 是 !ok 的,
      // 下面把值存到 dirty map 里面了。
      m.read.Store(readOnly{m: read.m, amended: true})
    }
    // 2. 这里,把值存到 dirty map 中。
    m.dirty[key] = newEntry(value)
  }
  m.mu.Unlock()
}

精妙绝伦!整个写入的逻辑就讲完了,最后看看遍历吧,非常简单:

func (m *Map) Range(f func(key, value interface{}) bool) {
  read, _ := m.read.Load().(readOnly)
  // 如果不一致,就把 dirty 提升为 readOnly,
  // 同时 dirty 置空,
  // 因为 dirty map 也包含了 readOnly map 没有的 key。
  if read.amended {
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if read.amended {
      read = readOnly{m: m.dirty}
      m.read.Store(read)
      m.dirty = nil
      m.misses = 0
    }
    m.mu.Unlock()
  }
  // 遍历 readOnly map 的数据,执行回调函数。
  for k, e := range read.m {
    v, ok := e.load()
    if !ok {
      continue
    }
    if !f(k, v) {
      break
    }
  }
}

好了,到这里整个 sync.Map 就讲完了,剩下的代码也没多少了,套路差不多,我们总结一下:

  1. 在读多写少的场景下,sync.Map 的性能非常高,因为访问 readOnly map 是无锁的;
  2. Load:先查找 readOnly map,找不到会去找 dirty map,如果经常没命中,dirty map 会被提升为 readOnly map,提升的时机跟 dirty 的大小相关,dirty 越大,容忍不命中的次数就越多,也就越难提升;
  3. Delete:当 readOnly map 的 key 不存在的时候,会去删除 dirty map 中的 key;如果 readOnly map 的 key 存在,entry.p 置为 nil;
  4. Store :

    1. readOnly map 的 key 存在时,entry.p != expunged 时直接更新,entry.p == expunged 就改成 nil,此时数据也同步写入 dirty map;
    2. readOnly map 的 key 不存在时,dirty map 有就更新进去,两个都没有,触发写时复制机制:搬迁 readOnly map 的没有被删除的 key 到 dirty map 中,新值写入 dirty map,并设置 amended 标记为 true。
  5. sync.Map 的缺陷在于读少写多的时候,dirty map 会被一直更新,misses 次数增加,dirty 置空后,数据又重新从 readOnly map 同步回去,使得 sync.Map 忙于数据搬迁工作,影响性能。

这篇文章近 5000 字(第一篇差不多 2000 字),从构思、成文到校对,真的需要花费不少时间,希望对你有帮助!