Ningx Unit与Golang

" NGINX Unit 是一个动态Web和应用服务器,可以运行多种编程语言的应用。Unit 是轻量级的、支持多种语言,并且可以通过API进行动态配置。Unit 的设计允许开发或运维需要的情况下,重新配置特定应用参数。 "

官网,当前版本1.22.0。

看起来支持的语言也不少Go、Python、PHP、Java…

官网安装介绍

Ubuntu 20.10 安装:

  1. curl -sL https://nginx.org/keys/nginx_signing.key | apt-key add -

  2. 创建文件 /etc/apt/sources.list.d/unit.list 并在里面添加如下内容:

deb https://packages.nginx.org/unit/ubuntu/ groovy unit

deb-src https://packages.nginx.org/unit/ubuntu/ groovy unit

  1. 安装 Unit 基础包

    apt update

    apt install unit

    apt install unit-dev unit-go unit-jsc11 unit-jsc13 unit-jsc14 unit-jsc15 unit-perl unit-php unit-python3.8 unit-ruby

    其实我只需要unit-go unit-dev unit-python

  2. 安装Go模块

  go get unit.nginx.org/go

命令:

systemctl enable unit 允许自动启动

systemctl restart unit 重启

systemctl stop unit

systemctl disable unit

一个go代码测试

package main

import (
	"fmt"
	"net/http"

	unit "unit.nginx.org/go"
)

func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "text/plain")

	fmt.Fprintf(w, "Method : %s\n", r.Method)
	fmt.Fprintf(w, "URL    : %s\n", r.URL.Path)
	fmt.Fprintf(w, "Host   : %s\n", r.Host)
}

func main() {
	http.HandleFunc("/", handler)
	unit.ListenAndServe(":8000", nil)
}

编译,虽然会提示NginxUnit未运行,不过它是可以独立访问的。有点尴尬的在于,通过apt安装的是go1.14版,而我本机已升级到1.16。另外,go get unit时,是需要安装mercurial,而非git。

查看通讯端口: sudo /usr/sbin/unitd 我的显示 unix:/var/run/control.unit.sock (用于替换示例中的进程端口)

查看当前的配置: sudo curl –unix-socket /var/run/control.unit.sock http://localhost

返回了一个空壳

{
	"certificates": {},
	"config": {
		"listeners": {},
		"applications": {}
	}
}
这是配置文件:unit.config

{
    "applications": {
		"example_go": {
			"type": "go",
			"executable": "/home/ease/soease/unit/t1"
		}
	},
	"listeners": {
		"*:8500": {
			"application": "example_go"
		}
	}
}

通过curl将unit.config文件内容传递给unitd程序,返回Reconfiguration done表示配置成功。

curl -X PUT -d @unit.config –unix-socket control.unit.sock http://localhost/config (解释:通过进程端口,将配置文件unit.config发送到本地配置的config节点下)

通过 curl 127.0.0.1:8500 即可检验是否成功。

修改测试1

配置文件我稍作了修改,中文也可以识别

{
    "applications": {
		"测试": {
			"type": "go",
			"executable": "/home/ease/soease/unit/t1"
		}
	},
	"listeners": {
		"*:8500": {
			"application": "测试"
		}
	}
}

修改测试2

再修改,两个端口调用同一程序

{
	"certificates": {},
	"config": {
		"applications": {
			"测试": {
				"type": "go",
				"executable": "/home/ease/go/my/src/github.com/soease/unit/t1"
			}
		},

		"listeners": {
			"*:8500": {
				"application": "测试"
			},

			"*:8080": {
				"application": "测试"
			}
		}
	}
}

修改测试3

//试试恢复为golang原装http

package main

import (
	"fmt"
	"net/http"
	//unit "unit.nginx.org/go"
)

func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Add("Content-Type", "text/plain")

	fmt.Fprintf(w, "t2")
	fmt.Fprintf(w, "Method : %s\n", r.Method)
	fmt.Fprintf(w, "URL    : %s\n", r.URL.Path)
	fmt.Fprintf(w, "Host   : %s\n", r.Host)
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8002", nil)
}

同时修改配置文件

{
    "applications": {
		"test1": {
			"type": "go",
			"executable": "/home/ease/soease/unit/t1"
		},
		"test2": {
			"type": "go",
			"executable": "/home/ease/soease/unit/t2"
		}        
	},
	"listeners": {
		"*:8500": {
			"application": "test1"
		},
        "*:8080": {
            "application": "test2"
        }
	}
}

可以通过ps看到,unit调用了t2,但发送配置文件被卡在那里,没有返回正确的信息。

将以上的golang代码中改回unit的web服务,以上配置就正常了。

通过多次试图使用原装http服务,都不成功。结论:必须调用unit模块的http服务模块。

修改测试4

t1的端口为8001,t2的端口为8002。看看这样的配置在unit会“打架”不。

{
    "applications": {
		"测试1": {
			"type": "go",
			"executable": "/home/ease/soease/unit/t1"
		},
		"测试2": {
			"type": "go",
			"executable": "/home/ease/soease/unit/t2"
		}        
	},
	"listeners": {
		"*:8001": {
			"application": "测试2"
		},
        "*:8002": {
            "application": "测试1"
        }
	}
}

结果:

curl localhost:8001 调用了t2

curl localhost:8002 调用了t1

感觉它在程序中的端口指定并没有实际意义。(后来在官方文档中看到,它会忽略golang中的端口设置,只管unit中的配置端口)

写header

package main

import (
	"fmt"
	"io"
	"net/http"
	"unit.nginx.org/go"
)

func handler(w http.ResponseWriter, r *http.Request) {
	var buf [4096]byte
	len, _ := r.Body.Read(buf[:])

	w.Header().Set("Request-Method", r.Method)
	w.Header().Set("Request-Uri", r.RequestURI)
	w.Header().Set("Server-Protocol", r.Proto)
	w.Header().Set("Server-Protocol-Major", fmt.Sprintf("%v", r.ProtoMajor))
	w.Header().Set("Server-Protocol-Minor", fmt.Sprintf("%v", r.ProtoMinor))
	w.Header().Set("Content-Length", fmt.Sprintf("%v", len))
	w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
	w.Header().Set("Custom-Header", r.Header.Get("Custom-Header"))
	w.Header().Set("Http-Host", r.Header.Get("Host"))

	io.WriteString(w, string(buf[:len]))
}

func main() {
	http.HandleFunc("/", handler)
	unit.ListenAndServe(":7080", nil)
}

问题1

如果代码中必须由unit来调用的话,那么Gin这种框架又要如何来适应呢? 若需要Gin框架开发者为修改的话,unit的适应性将打折扣。

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

在官方文档中框架的使用,不少Python和PHP的框架应用,没找到golang的。

我试着通过Python的示例来解决。

看看Python的两个框架示例:

Flask

文件wsgi.py

from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello, World!"

chown -R unit:unit /path/to/app/ 让unit的用户unit能够有运行权限

{
    "listeners": {
        "*:80": {
            "pass": "applications/flask"
        }
    },

    "applications": {
        "flask": {
            "type": "python 3.x",
            "path": "/path/to/app/",
            "home": "/path/to/app/venv/",
            "module": "wsgi",
            "callable": "app"
        }
    }
}
Bottle

文件wsgi.py

from bottle import Bottle, template

app = Bottle()

@app.route('/hello/<name>')
def hello(name):
    return template('Hello, {{name}}!', name=name)

# run(app, host='localhost', port=8080)  注意,这里注释掉了run

chown -R unit:unit /path/to/app/  让unit的用户unit能够有运行权限

{
    "listeners": {
        "*:80": {
            "pass": "applications/bottle"
        }
    },

    "applications": {
        "bottle": {
            "type": "python x.y",
            "path": "/path/to/app/",
            "home": "/path/to/app/venv/",
            "module": "wsgi",
            "callable": "app"
        }
    }
}

我修改配置文件

{
    "listeners": {
        "*:8888": {
            "pass": "applications/mygo"
        }
    },

    "applications": {
        "mygo": {
            "type": "go",
            "executable":"/home/ease/go/my/src/github.com/soease/unit/t6"
        }
    }
}
package main

import (
	"io"
	"net/http"
	//"unit.nginx.org/go"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "Hello, Go on Unit!!!")
	})
	http.ListenAndServe(":8081", nil)
}

发现:在发送配置文件时,配置文件不成功,卡死在发送处,没有反馈信息;unit已运行t6服务,t6能正常访问。

结果:失败!

问题2

当我重新编译了其中一个服务,必须得重启unit服务,重载配置也没用。估计是还有地方没学会。

完整的配置示例

{
    "certificates": {
        "bundle": {
            "key": "RSA (4096 bits)",
            "chain": [
                {
                    "subject": {
                        "common_name": "example.com",
                        "alt_names": [
                            "example.com",
                            "www.example.com"
                        ],

                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme, Inc."
                    },

                    "issuer": {
                        "common_name": "intermediate.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Certification Authority"
                    },

                    "validity": {
                        "since": "Sep 18 19:46:19 2018 GMT",
                        "until": "Jun 15 19:46:19 2021 GMT"
                    }
                },

                {
                    "subject": {
                        "common_name": "intermediate.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Certification Authority"
                    },

                    "issuer": {
                        "common_name": "root.ca.example.com",
                        "country": "US",
                        "state_or_province": "CA",
                        "organization": "Acme Root Certification Authority"
                    },

                    "validity": {
                        "since": "Feb 22 22:45:55 2016 GMT",
                        "until": "Feb 21 22:45:55 2019 GMT"
                    }
                }
            ]
        }
    },

    "config": {
        "settings": {
            "http": {
                "header_read_timeout": 10,
                "body_read_timeout": 10,
                "send_timeout": 10,
                "idle_timeout": 120,
                "max_body_size": 6291456,
                "static": {
                    "mime_types": {
                        "text/plain": [
                             ".log",
                             "README",
                             "CHANGES"
                        ]
                    }
                },
                "discard_unsafe_fields": false
            }
        },

        "listeners": {
            "*:8000": {
                "pass": "routes",
                "tls": {
                    "certificate": "bundle"
                }
            },

            "127.0.0.1:8001": {
                "pass": "applications/drive"
            },

            "*:8080": {
                "pass": "upstreams/rr-lb"
            }
        },

        "routes": [
            {
                "match": {
                    "uri": "/admin/*",
                    "scheme": "https",
                    "arguments": {
                        "mode": "strict",
                        "access": "!raw"
                    },

                    "cookies": {
                        "user_role": "admin"
                    }
                },

                "action": {
                    "pass": "applications/cms"
                }
            },
            {
                "match": {
                    "host": "admin.emea-*.*.example.com",
                    "source": "*:8000-9000"
                },

                "action": {
                    "pass": "applications/blogs/admin"
                }
            },
            {
                "match": {
                    "host": ["blog.example.com", "blog.*.org"],
                    "source": "*:8000-9000"
                },

                "action": {
                    "pass": "applications/blogs/core"
                }
            },
            {
                "match": {
                    "host": "example.com",
                    "source": "127.0.0.0-127.0.0.255:8080-8090",
                    "uri": "/chat/*"
                },

                "action": {
                    "pass": "applications/chat"
                }
            },
            {
                "match": {
                    "host": "example.com",
                    "source": [
                        "10.0.0.0/7:1000",
                        "10.0.0.0/32:8080-8090"
                    ]
                },

                "action": {
                    "pass": "applications/store"
                }
            },
            {
                "match": {
                    "host": "wiki.example.com"
                },

                "action": {
                    "pass": "applications/wiki"
                }
            },
            {
                "match": {
                     "uri": "/legacy/*"
                },

                "action": {
                    "return": 301,
                    "location": "https://legacy.example.com"
                }
            },
            {
                "match": {
                    "scheme": "http"
                },

                "action": {
                    "proxy": "http://127.0.0.1:8080"
                }
            },
            {
                "action": {
                    "share": "/www/static/",
                    "fallback": {
                        "proxy": "http://127.0.0.1:9000"
                    }
                }
            }
        ],

        "applications": {
            "blogs": {
                "type": "php",
                "targets": {
                    "admin": {
                        "root": "/www/blogs/admin/",
                        "script": "index.php"
                    },

                    "core" : {
                        "root": "/www/blogs/scripts/"
                    }
                },

                "limits": {
                    "timeout": 10,
                    "requests": 1000
                },

                "options": {
                    "file": "/etc/php.ini",
                    "admin": {
                        "memory_limit": "256M",
                        "variables_order": "EGPCS",
                        "expose_php": "0"
                    },

                    "user": {
                        "display_errors": "0"
                    }
                },

                "processes": 4
            },

            "chat": {
                "type": "external",
                "executable": "bin/chat_app",
                "group": "www-chat",
                "user": "www-chat",
                "working_directory": "/www/chat/",
                "isolation": {
                    "namespaces": {
                        "cgroup": false,
                        "credential": true,
                        "mount": false,
                        "network": false,
                        "pid": false,
                        "uname": false
                    },

                    "uidmap": [
                        {
                            "host": 1000,
                            "container": 0,
                            "size": 1000
                        }
                    ],

                    "gidmap": [
                        {
                            "host": 1000,
                            "container": 0,
                            "size": 1000
                        }
                    ],

                    "automount": {
                        "language_deps": false,
                        "procfs": false,
                        "tmpfs": false
                    }
                }
            },

            "cms": {
                "type": "ruby",
                "script": "/www/cms/main.ru",
                "working_directory": "/www/cms/"
            },

            "drive": {
                "type": "perl",
                "script": "app.psgi",
                "threads": 2,
                "thread_stack_size": 4096,
                "working_directory": "/www/drive/",
                "processes": {
                    "max": 10,
                    "spare": 5,
                    "idle_timeout": 20
                }
            },

            "store": {
                "type": "java",
                "webapp": "/www/store/store.war",
                "classpath": ["/www/store/lib/store-2.0.0.jar"],
                "options": ["-Dlog_path=/var/log/store.log"]
            },

            "wiki": {
                "type": "python",
                "module": "asgi",
                "protocol": "asgi",
                "callable": "app",
                "environment": {
                    "DJANGO_SETTINGS_MODULE": "wiki.settings.prod",
                    "DB_ENGINE": "django.db.backends.postgresql",
                    "DB_NAME": "wiki",
                    "DB_HOST": "127.0.0.1",
                    "DB_PORT": "5432"
                },

                "path": "/www/wiki/",
                "processes": 10
            }
        },

        "upstreams": {
            "rr-lb": {
                "servers": {
                    "192.168.1.100:8080": { },
                    "192.168.1.101:8080": {
                        "weight": 2
                    }
                }
            }
        },

        "access_log": "/var/log/access.log"
    }
}

配置

删除对象 curl -X DELETE –unix-socket /path/to/control.unit.sock ‘http://localhost/config/listeners/*:8400’

type: external (Go and Node.js), java, perl, php, python, or ruby

environment: 环境变量,用于程序调用

{
    "type": "python 3.6",
    "processes": 16,
    "working_directory": "/www/python-apps",
    "path": "blog",
    "module": "blog.wsgi",
    "user": "blog",
    "group": "blog",
    "limits": {
        "timeout": 10,
        "requests": 1000
    },

    "environment": {
        "DJANGO_SETTINGS_MODULE": "blog.settings.prod",
        "DB_ENGINE": "django.db.backends.postgresql",
        "DB_NAME": "blog",
        "DB_HOST": "127.0.0.1",
        "DB_PORT": "5432"
    }
}

相关文章