nginx sticky session

Sticky session,一般翻译作会话保持,其实就是说负载均衡能做到这一点:一个用户第一次访问服务,可能是随机分配的一台upstream服务器提供服务,而这个用户的后续请求都发往第一次服务的这台机器。这样做有很多好处,比如可以提高用户数据的缓存命中率、数据一致性更容易保证等等。

Nginx本身就有一些会话保持的方法,比如ip_hash,根据请求的ip地址来哈希分配。但有些情况ip_hash是失效的,比如我们使用公司的网络,出口ip可能全都一样,这样我们访问一个外部服务其实全哈希到同一台服务器,直接失去了负载均衡的意义。Nginx Plus也有这个功能,看起来很美,唯一缺点是花钱。也有免费的sticky modulesticky module ng,问题是Nginx要重新编译,不太方便,也可能是一些新的大坑,时间黑洞。

其实我们用各种linux发行版维护的免费版Nginx配合上游服务器也能实现同样的功能。

原理很简单:应用服务器在用户请求里写入一个约定好的cookie,带上服务器的身份信息,比如主机名。Nginx先配置好{身份信息:地址}的映射关系,拿到请求后,在转发前提取此cookie,根据身份信息转发对应的upstream。

假设我们有两台应用服务器:node1, node2。cookie长这样 Name: AUTH_SESSION_ID, Value: a2c1dac1-5b50-4d1a-ba8f-f838222bd176.node1,其中node1指示了cookie来自哪台主机。这是Keycloak的cookie格式,我复用一下,其实你完全可以自己定义一种,就免了下面的正则表达式提取。

Nginx配置文件

http {
    map $cookie_AUTH_SESSION_ID $sticky_host {
        default default_upstream;
        ~^[^\.]*\.(?<node>.*) $node;
    }

    upstream node1 {
            server 4.3.2.1:8080 max_fails=1 fail_timeout=3s;
            server 4.3.2.2:8080 backup;
    }

    upstream node2 {
            server 4.3.2.2:8080 max_fails=1 fail_timeout=3s;
            server 4.3.2.1:8080 backup;
    }

    server {
            listen 80;
            location / {
                proxy_pass http://$sticky_host;
            }
    }
}

只需要提取cookie,转发对应的upstream,就实现了sticky session。
这里使用了map将AUTH_SESSION_ID的最后一部分提取到变量$sticky_host里,然后作为proxy_pass的值。

unknown collation utf8mb4_0900_ai_ci

如果使用了 mysql-connector-python 8.0.17 ,而你的mysql服务器并不是8.0,那么你可能会遇到这个错误。之前我们的解决方法是把版本退回到8.0.16,现在官方给出了真正的原因

解决办法是把连接串的参数charset=utf8mb4改为charset= utf8mb4_general_ci 。原因是从8.0.17开始,默认的utf8mb4其实会使用utf8mb4_0900_ai_ci 字符集编码,虽然mysql 8.0之前的服务器并不支持这个最新的编码。

使用企业微信 (Identity Provider) 登录Keycloak (Identity Broker)

Keycloak是一套不错的IAM解决方案,它能够实现SSO,还可以作为Identity Broker集成多种第三方登录方式。Keycloak自带常见的Social Login,包括Google、GitHub、Twitter等等,但是没有微信和企业微信。鉴于我司企业微信的广泛使用的事实, 在实现企业SSO服务过程中我们决定将其集成为主要第三方登录方式,基于Keycloak 6.0.1进行企业微信Identity Provider研发,本文记录了个人在此中积累的一点经验。本人主力语言并不是Java,文章有错误或您有更佳方案,请指正。

首先感谢https://gitee.com/jyqq163/keycloak-services-social-weixin 这个项目,指路明灯!企业微信与微信差异不大,我们做些改造即可。

查看项目里的WeixinIdentityProvider类,我们发现它extends AbstractOAuth2IdentityProvider;,这个org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider就是我们要实现的目标。

其实这个类不完全是问题的关键,因为你上来直接去看,不一定看得懂,所以问题的关键是要熟悉OAuth2的基本概念和常用认证流程。比如,Authentication和Authorization的区别是什么?OAuth2和OpenID Connect(OIDC)是什么关系?OAuth2里面Resource Owner是什么,Client是什么,Identity Provider是什么,User Federation又是怎么回事?常见的Authorization Grant有几种?分别是什么?哪两种比较常见?

超过一半答不上来的话,没有捷径,请老老实实先翻资料:https://tools.ietf.org/html/rfc6749 。不过说实话RFC文档并不适合阅读,你应该找其他面向人类的资料,比如http://gen.lib.rus.ec/book/index.php?md5=3707B342B22E5C059B2F17FEF8AB7D2F 。But when in doubt, read RFC。

另外你还要熟悉企业微信并不是100%符合标准的”OAuth2″认证流程,比如单独获取的、7200秒有效的、需要你缓存的access_token并不是 OAuth2 标准里的用户级access_token,而是全局的,它不是在认证第二步用code换回的,而是用corpId(clientId)和corpSecret(clientSecret)直接获取,是相对独立的流程。更多内容可参考https://zhuanlan.zhihu.com/p/36320213

有了清晰的概念之后就好办了。SSO一般使用流程是:用户想使用一个业务,使用客户端(一般是浏览器)访问业务服务器,业务服务器检查用户当前session状态,如果没有或过期,则向SSO服务检查用户是否已统一登录。如果SSO已登录则直接刷新自己的session,为用户提供服务;如果SSO未登录,则将用户重定向到SSO登录页。

此时用户有多种登录方式可选,我们这里是:直接登录Keycloak,或企业微信登录。用户选择企业微信登录后,Keycloak作为broker向向企业微信请求access_token,再向authorize endpoint发送指定appid、redirect_uri、response_type(grant type)、scope等参数的认证请求(链接/扫码),企业微信返回code后,再次利用access_tokencode请求用户身份信息,还可进一步获取用户详细信息,用这些信息查询或建立新用户,完成登录。最后重定向回到业务服务器,业务服务器即可向用户提供服务。

回到代码,我们现在的任务就是仿照WeixinIdentityProvider类,实现一个WechatWorkIdentityProvider类,让Keycloak认得企业微信。

从认证请求开始,方法名叫performLogin,看它基类的介绍Initiates the authentication process,这就是social login的起点。参考https://work.weixin.qq.com/api/doc#90000/90135/91022网页授权登录(这种方式在企业微信内打开有效,此外还有另一种二维码方式,可在企业微信外部打开。因此你可以根据user agent是否包含wxwork字样,生成不同的授权链接,余下的步骤是一样的),我们不难将链接的构造方法createAuthorizationUrl改造为适合企业微信的。然后Response.seeOther(authorizationUrl).build()其实就重定向、访问了企业微信授权接口,此接口带着codestate参数回调你指定的redirect_uri

redirect_uri按照是自动配置的,我们现在无需过多关注,需要关注的是其处理逻辑,是在内部类Endpoint里的authResponse方法,重点关注authorizationCodecode这个参数,还有真正获取、设置用户信息的getFederatedIdentity方法。根据企业微信文档,你需要拿code再加上单独获取的access_token来获取访问用户身份https://work.weixin.qq.com/api/doc#90000/90135/91023 ,取得UserId。随后还能进一步获取用户在企业微信通讯录里的详细信息:https://work.weixin.qq.com/api/doc#10019 。在extractIdentityFromProfile方法中,你可以将信息填入BrokeredIdentityContext,这样我们的扩展插件经手的工作就结束了,剩余的交给Keycloak处理。

注意企业微信比微信多了一个AgentId属性,Keycloak修改前端页面就能直接添加字段, 使用的是angular框架。我们可以在/keycloak-services-social-weixin/templates/realm-identity-provider-wechat-work.html里找到clientId属性,具体是

            <div class="form-group clearfix">
                <label class="col-md-2 control-label" for="clientId"><span class="required">*</span> {{:: 'pc-weixin-appid' | translate}}</label>
                <div class="col-md-6">
                    <input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.clientId" required>
                </div>
                <kc-tooltip>{{:: 'social.client-id.tooltip' | translate}}</kc-tooltip>
            </div>

其实对应到企业微信就是CorpID。复制一段,把信息全改为AgentID

            <div class="form-group clearfix">
                <label class="col-md-2 control-label" for="agentId"><span class="required">*</span> {{:: 'Agentid' | translate}}</label>
                <div class="col-md-6">
                    <input class="form-control" id="agentId" type="text" ng-model="identityProvider.config.agentId" required>
                </div>
                <kc-tooltip>{{:: 'social.agent-id.tooltip' | translate}}</kc-tooltip>
            </div>

这就为AgentID找到了保存的地方。可以在Provider里调用。内部怎样调取AgentId不具体细说了,看看源码,扩展OAuth2IdentityProviderConfig增加getAgentId方法,很容易的。

最后修改pom.xml文件,把项目名等信息改为 wechat-work ,调用mvn clean package把项目编译为独立Jar包,放入 KEYCLOAK_HOME/providers/(不存在就创建),把templates/*放入 KEYCLOAK_HOME/themes/base/admin/resources/partials/,重启Keycloak服务就可以在Identity Providers里找到wechat-work,新建,填写CorpID, AgentID, CorpSecret,随后在首页就会出现WechatWork登录渠道。

虽然文档异常缺乏,mailing list提问也没人理,Wildfly依赖关系花了一个星期才弄明白,但为Keycloak做功能扩展确实算很容易了,插件式的集成方式完全不用侵入其源码,这一点让人觉得选Keycloak还是正确的。

源码在此: https://github.com/kkzxak47/keycloak-services-social-wechatwork

PS: 为了缓存企业微信的access_token,我直接使用了org.infinispan.Cache,因为Keycloak自己就使用了这个缓存组件,我就不用再增加依赖了。但最后还是遇到了依赖问题Uncaught server error: java.lang.NoClassDefFoundError: org/infinispan/configuration/cache/ConfigurationBuilder,原本在项目pom.xml里添加了org.infinispan只在编译时有效,最后部署时,还是要在KEYCLOAK_HOME/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml里的dependencies里添加依赖,module.xml文件最后长这样:

<module name="org.keycloak.keycloak-services" xmlns="urn:jboss:module:1.3">
    <properties>
        <property name="jboss.api" value="private"/>
    </properties>

    <resources>
        <resource-root path="keycloak-services-6.0.1.jar"/>
    </resources>

    <dependencies>
        <module name="org.infinispan" services="import"/>
        <module name="org.keycloak.keycloak-common" services="import"/>
    ...
    </dependencies>
</module>

ansible-runner的用法

虽然ansible是Python项目,但在Python里调用ansible并没有想象中那么简单,很繁杂。ansible-runner是个不错的项目,提供了调用ansible的快捷方式。但文档异常缺乏,下面举两个例子,分别是调用module和调用ansible-playbook。

调用module非常简单,比如ansible all -m shell -a 'cat /proc/cpuinfo | grep processor | wc -l'直接写成

r = ansible_runner.run(inventory=inventory, host_pattern="default", module='shell', module_args="cat /proc/cpuinfo | grep processor | wc -l")

inventory就是个字符串,可以使用ini格式,比如:

"""[default] 
host_name ansible_host=1.2.3.4 ansible_port=22 ansible_user=root ansible_password=123456"""

inventory也可以通过跳板机,注意要安装sshpass

"""[default]
host_name ansible_host={ip} ansible_port={port} ansible_user={user} ansible_password={passwd} ansible_ssh_common_args='-o ProxyCommand="sshpass -p {jump_server_passwd} ssh -W %h:%p  {jump_server_user}@{jump_server_ip} -p {jump_server_port} """

调用ansible-playbook

r = ansible_runner.run(
                private_data_dir=tmpdir,
                inventory=inventory,
                playbook=playbook_path,
                host_pattern="default",
                # quiet=True,
                extravars={"timeout": 300, 
                           "lock_timeout": 120,
                           "service": 'test'
                           },
                envvars={"ANSIBLE_HOST_KEY_CHECKING": False}
            )

private_data_dir指向一个临时目录,可以用tempfile.TemporaryDirectory。

playbook_path是ansible-playbook的文件路径。host_pattern跟inventory保持一致为’default’即可。

quiet设为True可以屏蔽详细的日志信息。

extravars用来指定ansible-playbook内部的变量。

envvars就是附加的环境变量。

这两个例子已经覆盖了大部分使用场景,更详细的参数比如ssh private key登录可以看官方文档

Python引用计数

Python里object都会有一个属性:被引用次数,垃圾回收的时候会用到,最简单的情形是引用计数=0,直接回收掉即可。其他复杂些的情形,如循环引用,则需要通过标记-清除和分代回收机制来进行。
sys.getrefcount(obj)可以查看一个object被引用的次数。有趣的是它给出的结果总是比实际数目多1,原因是调用函数时也增加了一次引用。
其文档https://docs.python.org/3.7/library/sys.html#sys.getrefcount是如此描述的:

sys.getrefcount(object)
Return the reference count of the object. The count returned is generally one higher than you might expect, because it includes the (temporary) reference as an argument to getrefcount().

“generally one higher”,是说一般情况下会多一,但特殊情况不会多?否则直接减一然后返回实际值就行了?
值得研究一下。

二手HP LaserJet 1020 提示“前端盖打开或缺少硒鼓”的错误排查

首先这个报错的设定真是特别2,因为真正的原因甚至不止这两种,而把多个错误放到一条提示里,违反了基本的行事逻辑,平白增加了排错的难度,真是懒到家了,这样做可能可以少用几根信号线,我只能认为这是节省成本节约到姥姥家了。

搜索得知此报错的原因千奇百怪,但都和我的情况不符,我这边前端盖显然没有打开,前端盖的传感器连杆也好好的,硒鼓也是半新的,硒鼓的金属触点也是好的。

折腾了半天,把打印机拎起来晃也没用。继续搜索,在惠普官网终于找到了相关信息:步骤二 插入硒鼓,提示有一个传感器未复位可能导致检测不到硒鼓。拆下硒鼓,找到机器里进纸口上方金属横杆边靠左侧的传感器,用手按压,发现传感器不能移动,用手反复按压,虽有严重阻滞,但传感器终于动了。按上硒鼓后,机器终于恢复。

不过此时打印的效果不佳,页面总有几道白线无墨,勉强打印了几页之后,机器又不动了。怒了拆下硒鼓摇晃,把机器也180°翻转过来。继续尝试,这时出纸口竟然有一片枯树叶和纸一起送出来,仔细看根本不是我这附近的树,只能说商家卖东西太不上心了,说发货前清理过,结果呢。

另外这个解决方案是放在“卡纸后无法继续打印”这个话题里,如果能加上如标题里的报错信息,就能让人直接检索到了。

跟医生看病一样,多个症状对应多个病因,只有对情况比较熟悉,才有可能找到真正原因。

UVa1600

1170: 暴躁机器人

时间限制: 1 Sec  内存限制: 128 MB
提交: 17  解决: 7
[提交] [状态] [讨论版] [命题人:test]

题目描述

PIPI发明了一个暴躁机器人,机器人位于一个 n*m 的网格中, 网格中的一些格子是空地(数字0表示),另一些是障碍物(数字1表示),暴躁机器人从左上角(1,1)出发前往右下角(n,m), 机器人每一步可以往上下左右四个方向走一个,由于机器人很暴躁,所以它可以摧毁障碍物。但是它最多连续穿过k 个障碍物,求机器人从左上角到右下角的最短路长度。 保证起点和终点是空地。

输入

输入包含一个正整数T,代表测试用例个数。 
对于每组测试用例,第一行包含三个整数 n,m,k (1<=n,m<=20, 0<=k <=20)。 
接下来包含一个 n*m 的01矩阵,代表网格矩阵。 

输出

对于每组测试用例,输出机器人从左上角走到右下角的最少步数。若无法到达右下角,输出 -1 。

样例输入

1
3 6 1
0 1 1 0 0 0
0 0 1 0 1 1
0 1 1 1 1 0

样例输出

9

提示

测试用例的走法: (1,1) -> (2,1) -> (2,2) -> (2,3) ->(2,4) -> (1,4) -> (1,5) -> (1,6) -> (2,6) -> (3,6) . 路径长度为 9. 

来源/分类

中等搜索

今天被这道题拦住了。

原因倒是不难理解,如果像传统搜索题那样只用二维数组记录一个坐标是否已被访问,由于访问路径的先后顺序问题,可能错过最优解或者错判为无解。所以关键在于怎样容纳多次访问重复的坐标,而且还要做必要的剪枝。

因为初始不熟悉这类问题,看答案都看不明白,不懂为什么只要用三维数组控制障碍数,不需要管其他指标,而且最后结果不需要去比较所有层级的最小值。

等到最后想明白究竟搜索时在控制什么指标,其他人的答案也就看明白了。

其实有两个重要指标:机器人走到某个坐标时,1. 走的步数,2. 当前剩余的突破能力。每个坐标都维护这些阈值,如果机器人走过来,走的步数小于当前坐标记录的最小值,或者突破能力大(如果此处是障碍),都可以入队列继续搜索,否则就抛弃,因为之前已经有更优的解了。

要理解别人的答案,注意几个点。由于BFS的特性,可以保证第一次走到此坐标时一定是最短距离。三维数组的第三维就是在记录到此为止突破的障碍数,每一层只访问一次,不重复,但不同的层级可以多次访问同一坐标,层级之间并没有顺序关系。剩余的突破能力应该越大越好,不过这一点可以忽略不去控制,多几次入队没有本质影响。因为突破能力弱的要么到不了终点,要么是之前拆墙走了捷径所以最终到达终点时用的步数可能要少一些。

#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <cmath>
#include <algorithm>
#include <queue>
#include <unordered_set>

using namespace std;

const int MAX = 1 << 30;

struct point {
  int x;  // location
  int y;
  int able;  // ability left to go through obstacle
  int steps;
  point(int x=0, int y=0, int able=0, int steps=0): x(x), y(y), able(able), steps(steps) {}
};

bool valid(int x, int y, vector<vector<int>> &arr) {
  return x >= 0 && x < arr.size() && y >= 0 && y < arr[0].size();
}


int main() {
  ios::sync_with_stdio(false);
  cin.tie(0);
  cout.tie(0);
  #ifndef ONLINE_JUDGE
  freopen("3.in", "r", stdin);
  freopen("3.out", "w", stdout);
  #endif
  int T;
  int n, m, k;
  cin >> T;
  for (int cs=1; cs <= T; cs++) {
    cin >> n >> m >> k;
    vector<vector<int>> arr(n, vector<int>(m));
    vector<vector<point>> v(n, vector<point>(m, point(0, 0, 0, MAX)));
    for(int i=0; i<n; i++) {
      for(int j=0; j<m; j++) {
        cin >> arr[i][j];
      }
    }
    queue<point> q;
    point p = point(0, 0, k, 0);
    q.push(p);
    v[0][0].steps = 0;
    v[0][0].able = k;

    int able;
    vector<int> m1 = {1, -1, 0, 0};
    vector<int> m2 = {0, 0, 1, -1};
    int x, y;
    while(!q.empty()) {
      p = q.front();
      q.pop();
      x = p.x;
      y = p.y;
      
      if(x == n-1 && y == m-1) {
        break;
      }
      for(int i=0; i<4; i++) {
        int nx = m1[i] + x;
        int ny = m2[i] + y;
        if(valid(nx, ny, arr) ) {
          bool flag = false;
          if(arr[nx][ny] == 0) {
            if(v[nx][ny].steps > p.steps + 1) {
              v[nx][ny].steps = p.steps + 1;
              q.push(point(nx, ny, k, p.steps+1));
            }
          }
          else if (p.able > 0) {
            if(v[nx][ny].steps > p.steps + 1) {
              v[nx][ny].steps = p.steps + 1;
            }
            else if(p.able > v[nx][ny].able) {
              v[nx][ny].able = p.able;
            }
            else {
              continue;
            }
            q.push(point(nx, ny, p.able-1, p.steps+1));
          }
        }
      }
    }

    if(v[n-1][m-1].steps != MAX) {
      cout << v[n-1][m-1].steps << "\n";
    }
    else {
      cout << "-1\n";
    }
  }
  return 0;
}

软件源被墙的解决办法

一般下载会用到wget和curl,那么为它们设置代理即可。

编辑 ~/.wgetrc

内容为:

use_proxy=on
http_proxy=127.0.0.1:8123
https_proxy=127.0.0.1:8123

编辑 ~/.curlrc

内容为:

proxy = localhost:8123

我使用polipo将SS转换为http代理,所以代理服务器地址是本地。

个人觉得这样的方式比较好,不用重启,只修改当前用户。不需要的时候可以用#把代理配置注释掉,还是比较方便的。

manjaro与video-vesa

手欠安装了video-vesa驱动,然后manjaro就无法启动了(表面上)。

第一思路是用启动盘进入live系统,chroot进入原系统,卸载错误驱动。这样确实可以解决问题,但其实杀鸡用了牛刀。(BTW:live系统root默认密码是manjaro)

显卡驱动只是导致图形界面无法启动,命令行是可以正常使用的。所以更快的解决办法是:

  1. Ctr-Alt F2 and log in
  2. sudo mhwd -r pci video-vesa
  3. Restart, and that’s it.

via: https://forum.manjaro.org/t/my-video-vesa-nightmare-solved/31505