índice
1.1 Descoberta de serviço do lado do cliente + balanceamento de carga
2 Como o Istio consegue o sequestro de tráfego?
3 Pergunta: Como julgar o tipo de serviço alvo?
1 O problema do microsserviço tradicional MicroService: descoberta de serviço intrusiva do lado do cliente + LoadBalance
1.1 Descoberta de serviço do lado do cliente + balanceamento de carga
Microsserviços tradicionais, descoberta de serviço + código de balanceamento de carga, são acoplados ao código de negócios e executados no mesmo processo que o negócio durante a operação.
Por exemplo, o serviço Tomcat iniciado pelo projeto Springboot, a lógica de negócios é executada neste tomcat e o código de descoberta de serviço e o código de balanceamento de carga após a descoberta de serviço também são executados neste tomcat.
Então, você pode separar o código de negócios e o código de estrutura?
Pode-se perceber que apenas o código de negócios é executado no servidor tomcat e a descoberta de serviço + balanceamento de carga é entregue a outros processos?
A resposta é sim. Coloque a descoberta de serviço + balanceamento de carga em um processo secundário separado, desacople-o do código de negócios e, ao mesmo tempo, obtenha proxy para tráfego de serviço por meio de sequestro de tráfego.
Um dos destaques do projeto do Istio é que os aplicativos antigos podem ser conectados perfeitamente à plataforma Service Mesh sem modificar uma linha de código. Para realizar esta função, atualmente, o tráfego é interceptado e encaminhado para o proxy através do iptables.
2 Como o Istio consegue o sequestro de tráfego?
Com referência à implementação do Istio, podemos projetar um esquema simples de sequestro de tráfego por nós mesmos.
2.1 O que precisa ser feito?
- Primeiro, deve haver um proxy que ofereça suporte a proxy transparente, que possa lidar com o tráfego sequestrado e ser capaz de obter o endereço de destino original quando a conexão for estabelecida. No k8s, esse proxy é implantado em um pod usando o método secundário e o serviço para sequestrar o tráfego.
- Sequestrar o tráfego que queremos sequestrar no proxy por meio de iptables. O tráfego do próprio proxy deve ser excluído.
- Para atingir a intrusão zero, é melhor não modificar a imagem do serviço.No k8s, o container Init pode ser usado para modificar o iptables antes do container do aplicativo iniciar.
2.2 Proxy transparente
Como um proxy transparente, o tráfego que ele pode manipular passará por uma série de lógicas de processamento, incluindo nova tentativa, tempo limite, balanceamento de carga etc., e então o encaminhará ao serviço de mesmo nível. Para o tráfego que não pode ser processado por si mesmo, ele será transmitido diretamente de forma transparente sem processamento.
Depois que o tráfego é encaminhado ao proxy por meio do iptables, o proxy precisa ser capaz de obter o endereço de destino original quando a conexão for estabelecida. A implementação em Go é um pouco mais problemática e precisa ser obtida syscall
chamando,
Código de amostra:
package redirect
import (
"errors"
"fmt"
"net"
"os"
"syscall"
)
const SO_ORIGINAL_DST = 80
var (
ErrGetSocketoptIPv6 = errors.New("get socketopt ipv6 error")
ErrResolveTCPAddr = errors.New("resolve tcp address error")
ErrTCPConn = errors.New("not a valid TCPConn")
)
// For transparent proxy.
// Get REDIRECT package's originial dst address.
// Note: it may be only support linux.
func GetOriginalDstAddr(conn *net.TCPConn) (addr net.Addr, c *net.TCPConn, err error) {
fc, errRet := conn.File()
if errRet != nil {
conn.Close()
err = ErrTCPConn
return
} else {
conn.Close()
}
defer fc.Close()
mreq, errRet := syscall.GetsockoptIPv6Mreq(int(fc.Fd()), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
if errRet != nil {
err = ErrGetSocketoptIPv6
c, _ = getTCPConnFromFile(fc)
return
}
// only support ipv4
ip := net.IPv4(mreq.Multiaddr[4], mreq.Multiaddr[5], mreq.Multiaddr[6], mreq.Multiaddr[7])
port := uint16(mreq.Multiaddr[2])<<8 + uint16(mreq.Multiaddr[3])
addr, err = net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%d", ip.String(), port))
if err != nil {
err = ErrResolveTCPAddr
return
}
c, errRet = getTCPConnFromFile(fc)
if errRet != nil {
err = ErrTCPConn
return
}
return
}
func getTCPConnFromFile(f *os.File) (*net.TCPConn, error) {
newConn, err := net.FileConn(f)
if err != nil {
return nil, ErrTCPConn
}
c, ok := newConn.(*net.TCPConn)
if !ok {
return nil, ErrTCPConn
}
return c, nil
}
O GetOriginalDstAddr
endereço de destino original da conexão pode ser obtido por meio da função.
É importante notar aqui que quando o encaminhamento de iptables está habilitado, se o proxy receber uma conexão que se acesse diretamente, ele reconhecerá que não pode tratá-la e, em seguida, se conectará ao endereço de destino (ou seja, o endereço vinculado a ele mesmo) , para que leve a um loop infinito. Portanto, quando o serviço é iniciado, a conexão cujo endereço de destino é o seu próprio IP precisa ser desconectada diretamente.
2.3 Sidecar
Ao usar o modo Sidecar para implantar a grade de serviço, um proxy adicional será aberto próximo a cada serviço para assumir parte do tráfego do contêiner. No kubernetes, um pod pode ter vários contêineres. Esses vários contêineres podem compartilhar recursos, como rede e armazenamento. Implante conceitualmente o contêiner de serviço e o contêiner de proxy em um pod, e o contêiner de proxy é equivalente a um contêiner de arquivo secundário.
Demonstramos por meio de uma implantação. A configuração yaml desta implantação inclui dois contêineres de teste e proxy, que compartilham a rede, portanto, após fazer login no contêiner de teste, 127.0.0.1:30000
você pode acessar o contêiner de proxy por meio.
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
namespace: default
labels:
app: test
spec:
replicas: 1
template:
metadata:
labels:
app: test
spec:
containers:
- name: test
image: {test-image}
ports:
- containerPort: 9100
- name: proxy
image: {proxy-image}
ports:
- containerPort: 30000
Escrever a configuração do contêiner do arquivo secundário para cada serviço é uma tarefa complicada. Quando a arquitetura estiver madura, podemos usar as MutatingAdmissionWebhook
funções do kubernetes para injetar ativamente configurações relacionadas ao arquivo secundário quando o usuário criar uma implantação.
Por exemplo, adicionamos os seguintes campos às anotações da implantação:
annotations:
xxx.com/sidecar.enable: "true"
xxx.com/sidecar.version: "v1"
Indica que a versão v1 do arquivo secundário precisa ser injetada nesta implantação. Quando nosso serviço recebe este webhook, ele pode verificar o campo de anotações relevantes e decidir se injeta a configuração do arquivo secundário e qual versão da configuração de acordo com a configuração do campo. Se houver alguns parâmetros que precisam ser alterados de acordo com o serviço, você também pode usar isso. A forma de entrega melhora muito a flexibilidade.
2,4 iptables
Por meio do iptables, podemos sequestrar o tráfego especificado para o proxy e excluir algum tráfego.
iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 9527 -j RETURN
iptables -t nat -A OUTPUT -p tcp -d 172.17.0.0/16 -j REDIRECT --to-port 30000
O comando acima significa transferir 172.17.0.0/16
o tráfego com o endereço de destino REDIRECT
para a porta 30000 (a porta que o proxy monitora). Mas o processo iniciado com UID 9527 é exceção. 172.17.0.0/16
Este endereço é o segmento IP dentro do cluster k8s. Precisamos apenas sequestrar esta parte do tráfego. Para o tráfego que acessa o exterior do cluster, não o sequestraremos temporariamente. Se todo o tráfego for sequestrado, as solicitações que o o proxy não pode manipular deve ser excluído por meio das regras do iptables.
2.5 container Init
Conforme mencionado anteriormente, a fim de atingir a intrusão zero, precisamos usar o container Init para modificar o iptables antes de iniciar o container de serviço do usuário. Essa parte da configuração também pode ser MutatingAdmissionWebhook
injetada na configuração de implantação do usuário por meio da função de kubernetes .
Adicione a configuração do contêiner Init à configuração do arquivo secundário anterior:
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
namespace: default
labels:
app: test
spec:
replicas: 1
template:
metadata:
labels:
app: test
spec:
initContainers:
- name: iptables-init
image: {iptables-image}
imagePullPolicy: IfNotPresent
command: ['sh', '-c', 'iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner 9527 -j RETURN && iptables -t nat -A OUTPUT -p tcp -d 172.17.0.0/16 -j REDIRECT --to-port 30000']
securityContext:
capabilities:
add:
- NET_ADMIN
privileged: true
containers:
- name: test
image: {test-image}
ports:
- containerPort: 9100
- name: proxy
image: {proxy-image}
ports:
- containerPort: 30000
Este container Init precisa instalar o iptables, e o comando iptables que configuramos será executado quando for iniciado.
Precisa de atenção extra é securityContext
este item de configuração, adicionamos NET_ADMIN
permissões. É usado para definir as permissões de Pod ou Container.Se não estiver configurado, o iptables irá avisar um erro ao executar o comando.
3 Pergunta: Como julgar o tipo de serviço alvo?
Nós 172.17.0.0/16
sequestramos todo o tráfego para o proxy, então como determinar o tipo de protocolo do serviço de destino? Se você não souber o tipo de protocolo, não terá certeza de como analisar as solicitações subsequentes.
No serviço kubernetes, podemos especificar um nome para a porta de cada serviço. O formato desse nome pode ser corrigido para {name}-{protocol}
, por exemplo {test-http}
, que uma determinada porta desse serviço seja um protocolo http.
kind: Service
apiVersion: v1
metadata:
name: test
namespace: default
spec:
selector:
app: test
ports:
- name: test-http
port: 9100
targetPort: 9100
protocol: TCP
O proxy obtém o IP do cluster e o nome da porta correspondente ao serviço por meio do serviço de descoberta, para que o tipo de protocolo de comunicação da conexão possa ser conhecido por meio do IP e da porta do serviço de destino, que pode então ser entregue ao manipulador correspondente Para processamento.
3.1 IP do cluster
Crie um serviço em kubernetes. Se não for especificado, o IP do cluster é usado para acessar por padrão. O Kube-proxy criará uma regra de iptables para isso, converterá o IP do cluster em balanceamento de carga e o encaminhará para o IP do pod.
Quando houver um IP do Cluster, a resolução DNS do serviço apontará para o IP do Cluster e o balanceamento de carga será feito pelo iptables. Se não existir, o resultado da resolução DNS apontará diretamente para o IP do pod.
O proxy depende do IP do cluster do serviço para determinar qual serviço o usuário está acessando, portanto, não pode ser definido clusterIP: None
. Como o IP do pod provavelmente muda com frequência, ao adicionar ou subtrair instâncias, o conjunto de IP do pod mudará e o proxy não poderá obter essas mudanças em tempo real.