Jupyterhub亂七八糟記錄

甲方和乙方的數據科學家都要用各種界面化工具來做數據科學家的工作,所以,我們從zeppelin搞到了jupyterlab,再從lab整到了hub。

對於甲方數據科學家的編程水平,實在是無法恭維卻還要硬着頭皮恭維。這輩子能看到把python寫成pig的機會不多。數據科學家的腦回路那是相當的PigLatinic的。這個,寫過pig的人應該能明白。

不過,我只想記錄一下工作過程,以後其他甲方要用的時候,我自己能用得上。


環境 Anaconda3 + Jupyterhub + Spark2.1 + CDH 5.14 + Kerberos


一、Hub與Spark的集成

anaconda怎麼裝jupyterhub和生成配置文件就不說了,網上一大堆。

鑑於數據科學家只會用python,所以基於toree的各種其他語言解釋器就先不記錄了。只有spark的話,還是挺簡單的。我是創建了一個文件

/usr/share/jupyter/kernels/pyspark2/kernel.json 路徑不存在的話就自己創建一個,文件不存在的話就自己vi一個。內容如下

{
      "argv": [
        "python3.6",
        "-m",
        "ipykernel_launcher",
        "-f",
        "{connection_file}"
      ],
      "display_name": "Python3.6+PySpark2.1",
      "language": "python",
      "env": {
        "PYSPARK_PYTHON": "/opt/anaconda3/bin/python",
        "SPARK_HOME": "/opt/spark-2.1.3-bin-hadoop2.6",
        "HADOOP_CONF_DIR": "/etc/hadoop/conf",
        "HADOOP_CLIENT_OPTS": "-Xmx2147483648 -XX:MaxPermSize=512M -Djava.net.preferIPv4Stack=true",
        "PYTHONPATH": "/opt/spark-2.1.3-bin-hadoop2.6/python/lib/py4j-0.10.7-src.zip:/opt/spark-2.1.3-bin-hadoop2.6/python/",
        "PYTHONSTARTUP": "/opt/spark-2.1.3-bin-hadoop2.6/python/pyspark/shell.py",
        "PYSPARK_SUBMIT_ARGS": " --master yarn --deploy-mode client --name JuPysparkHub測試 pyspark-shell"
      }
}

然後就沒有然後了。



二、Jupyterhub + 無LDAP的獨立Kerberos集成

由於集羣是獨立的Kerberos體系,並沒有與系統的PAM和LDAP結合起來,所以,這裏需要修改Jupyter代碼。

如果集羣是KRB5與LDAP結合的系統,可以忽略改代碼,Jupyterhub本身官方就有LDAP認證的插件。

基於我對Jupyterhub源碼的理解,Hub本身啓動時是啓動了一個多用戶認證的外殼程序,這個外殼程序會基於對每個用戶的認證,使用該用戶的linux賬號去啓動notebook。

image.png

這是我閱讀Hub源碼總結的架構流程,不一定準確,大概是個意思吧。NB就是NoteBook,不是牛逼的意思。


所以Hub其實最終啓動Notebook啓動的是一個新的jupyter-notebook的進程,而且是以登錄用戶的環境變量啓動的。那麼就有兩種思路來初始化kerberos principal。

思路一:

    使用系統用戶的環境變量來做kinit,好處是kinit寫在用戶的.bash_profile或.bashrc文件裏,只要調用這個用戶的環境變量就可以直接kinit,無需修改任何代碼。壞處是,要是kinit過期了,就得重新打開notebook,存在可能會導致編輯內容丟失的風險。

思路二:

    修改hub相關代碼,將kinit做到nb裏面去,這樣就無所謂系統環境變量,尤其hub有個自動保存的機制,前端會定期發送保存筆記的請求給後端api,如果找到api就可以完成這個功能,這樣kinit就是定期刷新的,只要開着nb,kerberos的ticket就永不過期。

    考慮到甲方數據科學家是一羣除了會寫pyspparksql而其他方面是毫無自理能力和學習能力的唐氏綜合徵,我決定採用第二種方案。給唐爸爸省事,也是給自己省事,一勞永逸。


那麼根據hub的流程架構和思路二,我只需要修改notebook本身的kinit認證即可。不過爲了保險,我把hub的登錄驗證外殼也一起改了。

由於hub和nb系列都是tornado寫的,改起來倒也容易。

首先是要寫一段簡單粗暴的python代碼來做kinit驗證。

    def kinit(self, username):
        """
        add by xianglei
        """
        import os
        from subprocess import Popen, PIPE
        uid_args = ['id', '-u', username]
        uid = Popen(uid_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
        uid = uid.communicate()[0].decode().strip()
        gid_args = ['id', '-g', username]
        gid = Popen(gid_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
        gid = gid.communicate()[0].decode().strip()
        self.log.info('UID: ' + uid + ' GID: ' + gid)
        self.log.info('Authenticating: ' + username)
        realm = 'XX.COM'
        kinit = '/usr/bin/kinit'
        krb5cc = '/tmp/krb5cc_%s' % (uid,)
        keytab = '/home/%s/%s.wb1.keytab' % (username, username)
        principal = '%s/pg-dmp-workbench1@%s' % (username, realm,)
        kinit_args = ['kinit', '-kt', keytab, '-c', krb5cc, principal]
        self.log.info('Running: ' + ' '.join(kinit_args))
        kinit = Popen(kinit_args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
        self.log.info(kinit.communicate())
        ans = None
        import os
        if os.path.isfile(krb5cc):
            os.chmod(krb5cc, 0o600)
            os.chown(krb5cc, int(uid), int(gid))
            ans = username
        return ans

都是直接調用操作系統命令,可以說是相當粗暴了。


然後找到 jupyterhub源碼包裏面的 auth.py,不要問我源碼在哪裏,如果找不到python第三方module的安裝位置,那麼跟數據科學家有什麼區別。

找到 authenticate方法,改成下面。

    @run_on_executor
    def authenticate(self, handler, data):
        """Authenticate with PAM, and return the username if login is successful.

        Return None otherwise.
        """
        username = data['username']
        try:
            pamela.authenticate(username, data['password'], service=self.service, encoding=self.encoding)
            username = self.kinit(username)
        except pamela.PAMError as e:
            if handler is not None:
                self.log.warning("PAM Authentication failed (%s@%s): %s", username, handler.request.remote_ip, e)
            else:
                self.log.warning("PAM Authentication failed: %s", e)
        else:
            if not self.check_account:
                return username
            try:
                pamela.check_account(username, service=self.service, encoding=self.encoding)
                username = self.kinit(username)
            except pamela.PAMError as e:
                if handler is not None:
                    self.log.warning("PAM Account Check failed (%s@%s): %s", username, handler.request.remote_ip, e)
                else:
                    self.log.warning("PAM Account Check failed: %s", e)
            else:
                return username

這樣 hub 在登錄的時候就可以先做一次kinit認證,其實沒什麼用。但當我面對比集羣機器還多的唐氏患者時,我還是需要一些心理安慰的。

然後找到 notebook 的 notebook/handler.py 文件,修改如下。

class NotebookHandler(IPythonHandler):

    @web.authenticated
    def get(self, path):
        """get renders the notebook template if a name is given, or
        redirects to the '/files/' handler if the name is not given."""
        path = path.strip('/')
        cm = self.contents_manager

        # will raise 404 on not found
        try:
            model = cm.get(path, content=False)
        except web.HTTPError as e:
            if e.status_code == 404 and 'files' in path.split('/'):
                # 404, but '/files/' in URL, let FilesRedirect take care of it
                return FilesRedirectHandler.redirect_to_files(self, path)
            else:
                raise
        if model['type'] != 'notebook':
            # not a notebook, redirect to files
            return FilesRedirectHandler.redirect_to_files(self, path)
        name = path.rsplit('/', 1)[-1]
        username = self.current_user['name']
        self.kinit(username)
        self.write(self.render_template('notebook.html',
            notebook_path=path,
            notebook_name=name,
            kill_kernel=False,
            mathjax_url=self.mathjax_url,
            mathjax_config=self.mathjax_config,
            get_custom_frontend_exporters=get_custom_frontend_exporters
            )
        )

以上代碼的作用是在打開notebook的時候就做kinit認證。


然後打開 notebook的 service/contents/handlers.py

    @gen.coroutine
    def _save(self, model, path):
        """Save an existing file."""
        chunk = model.get("chunk", None)
        if not chunk or chunk == -1:  # Avoid tedious log information
            self.log.info(u"Saving file at %s", path)
            if 'name' in self.current_user:
                if isinstance(self.current_user['name'], str):
                    self.kinit(self.current_user['name'])
                    #pass
        model = yield gen.maybe_future(self.contents_manager.save(model, path))
        validate_model(model, expect_content=False)
        self._finish_model(model)

以上代碼的作用是在notebook做自動或手動保存時就觸發kinit認證。

各種保險措施做足,國際臉的唐氏患者們表示生活很幸福快樂,露出了久違的微笑。



三、hub與nginx反代集成多域名和SSL。

我作爲一個正常人是永遠猜不透唐氏患者內心在想什麼,他們永遠有辦法讓乙方永無休止的幹活,或許這樣他們就能打着系統升級的旗號不幹活了。所以,IP不能訪問嗎?爲啥非要弄個域名呢?

而且我們的集羣環境是分爲業務***和管理***的,兩個***綁定的域名是不一樣的,然後都需要SSL連接。

這裏有點麻煩的是,Jupyter是禁止跨域訪問的。SSL加上反代其實配置不難,難的是跨域訪問,其實跨域訪問也不難,難的是如何殺死這些數據科學家。

upstream hub10000 {
        server 172.16.191.110:10000;
    }

server {
        listen       30000;
        server_name     mgmthub.xxx.cn buhub.xxx.cn;

        ssl on;
        ssl_certificate_key cert/xxx.cn.key;
        ssl_certificate cert/xxx.cn.crt;

        ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-RC4-SHA:!ECDHE-RSA-RC4-SHA:ECDH-ECDSA-RC4-SHA:ECDH-RSA-RC4-SHA:ECDHE-RSA-AES256-SHA:!RC4-SHA:HIGH:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!CBC:!EDH:!kEDH:!PSK:!SRP:!kECDH;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_session_cache shared:SSL:10m;
        ssl_prefer_server_ciphers on;
        ssl_session_timeout 1d;
        ssl_stapling on;
        ssl_stapling_verify on;

        add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";
        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header Content-Security-Policy "default-src 'self';style-src 'self' 'unsafe-inline';script-src 'self' 'unsafe-inline' 'unsafe-eval';font-src 'self' data:;connect-src 'self' wss:;img-src 'self' data:;";

        location / {
          proxy_pass http://hub10000;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "upgrade";
          proxy_set_header Origin "";
          include proxy.conf;
        }
    }


proxy.conf

proxy_redirect          off;
proxy_set_header        Host $host;
proxy_set_header        X-Real-IP $remote_addr;
proxy_set_header        X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_set_header        X-Nginx-Proxy true;
proxy_connect_timeout   604800;
proxy_send_timeout      604800;
proxy_read_timeout      604800;

proxy_buffer_size       64k;
proxy_buffers           64 32k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 64k;
proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;
proxy_max_temp_file_size 128m;

client_body_temp_path client_body 1 2;
proxy_temp_path proxy_temp 1 2;


110:10000是jupyterhub監聽地址,是另一臺機器。

然後加上  

proxy_set_header Origin "";

就可以解決tornado的跨域訪問問題,無需修改jupyterhub的配置裏的 bind_url設置。


對文中提到唐氏患者致歉,雖然我知道用唐氏患者類比甲方數據科學家是對唐氏患者的不尊重,但才疏學淺,確實找不到合適的詞彙來比喻甲方數據科學家。我本身並不歧視唐氏患者,我歧視的是數據科學家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章