การตรวจจับสภาพการแข่งขันด้วยตัวอย่าง Reverse Proxy

สภาพการแข่งขันถือเป็นข้อบกพร่องของซอฟต์แวร์ที่ละเอียดอ่อนแต่ก็สร้างความเสียหายร้ายแรง

Chat GPT อธิบายว่า:

ข้อบกพร่องของซอฟต์แวร์ที่เกิดขึ้นเมื่อความถูกต้องของโปรแกรมขึ้นอยู่กับระยะเวลาสัมพัทธ์หรือการสลับกันของการดำเนินการหลายรายการพร้อมกัน

คำนี้มักใช้เป็นทางลัดทางจิตเพื่ออธิบายพฤติกรรมของซอฟต์แวร์ที่ไม่สามารถอธิบายได้

สิ่งเหล่านี้เป็นบ่อเวลาครั้งใหญ่และเป็นบ่อเกิดของความคับข้องใจไม่รู้จบ

มนุษย์และสมองแบบเธรดเดียวมักจะตรวจไม่พบพวกมัน ไม่สำคัญว่าคุณคิดว่าคุณทำงานหลายอย่างพร้อมกันได้ดีเพียงใด หากคุณกำลังเล่นพร้อมกัน คุณจะเหนื่อยหน่ายกับสภาพการแข่งขัน

ที่แย่กว่านั้นคือ มีโอกาสที่ดีที่คุณจะสังเกตเห็นปัญหาได้ดีหลังจากที่คุณปรับใช้การเปลี่ยนแปลงกับการใช้งานจริง

สถานการณ์หนึ่งเกิดขึ้นในยุค 80 ด้วย Therac 25 ซึ่งเป็นเครื่องฉายรังสีบำบัดแบบสองโหมดที่ปฏิวัติวงการ ในแง่ของคนธรรมดา มันมีโหมดพลังงานต่ำและโหมดพลังงานสูง

ช่างผู้ชำนาญสามารถพิมพ์คำสั่งเพื่อตั้งค่าเครื่องได้ในพริบตา

วันแห่งโชคร้ายวันหนึ่ง ช่างเทคนิคได้เกิดข้อผิดพลาดในการตั้งค่าเครื่องสำหรับโหมด X (เอ็กซเรย์) แทนที่จะเป็นโหมด e (อิเล็กตรอน) ช่างสังเกตเห็นข้อผิดพลาดจึงรีบแก้ไข

เครื่องหยุดทำงานและแสดงข้อผิดพลาด “Malfunction 54” ช่างเทคนิคตีความข้อผิดพลาดว่าเป็นปัญหาที่มีลำดับความสำคัญต่ำและดำเนินการตามกระบวนการต่อไป

ผู้ป่วยรายงานว่าได้ยินเสียงหึ่งดังตามมาด้วยความรู้สึกแสบร้อนราวกับว่ามีคนเทกาแฟร้อนลงบนผิวหนัง

สองสามวันต่อมา ผู้ป่วยป่วยเป็นอัมพาตเนื่องจากการได้รับรังสีมากเกินไป และเสียชีวิตในไม่ช้า ผู้ป่วย 6 รายเสียชีวิตเนื่องจากข้อผิดพลาด "ความผิดปกติ 54" ระหว่างปี 1985 ถึง 1987

การสอบสวนในภายหลังพบว่าสภาพการแข่งขันในซอฟต์แวร์ของเครื่องทำให้เกิดเหตุการณ์ดังกล่าว

มีหลายสิ่งที่ต้องแกะออกมาในเรื่องคำเตือนของ Therac 25 สำหรับวัตถุประสงค์ของบทความนี้ เราจะเน้นไปที่ส่วนเล็กๆ ที่เกี่ยวข้องกับสภาพการแข่งขัน

ในส่วนถัดไป เราจะสวมหมวกแก้ไขจุดบกพร่องและสำรวจสภาพการแข่งขันในสภาพแวดล้อมที่ปลอดภัยยิ่งขึ้น

การตั้งเวที

เราต้องการใช้พร็อกซีย้อนกลับ HTTP ที่จะส่งต่อคำขอไปยังระบบที่เหมาะสมตามเงื่อนไขบางประการ

พร็อกซีย้อนกลับคือเซิร์ฟเวอร์ตัวกลางที่รับคำขอจากไคลเอ็นต์และนำคำขอเหล่านั้นไปยังเซิร์ฟเวอร์แบ็กเอนด์ โดยทั่วไปจะใช้เพื่อจัดการกับข้อกังวลด้านประสิทธิภาพ ความปลอดภัย และความสามารถในการปรับขนาด

หนึ่งในตัวอย่างของเราส่งต่อคำขอไปยังระบบ A หรือ B ขึ้นอยู่กับเส้นทางของคำขอดั้งเดิม เส้นทางคำขอดั้งเดิมจะต้องถูกแมปกับเส้นทางอัปสตรีมที่เหมาะสม

ตัวอย่างเช่น:

api.com/a/foo/bar -> system-a.com/v1/foo/bar

api.com/b/baz/14 -> system-b.com/baz/14

โดยทั่วไปจะใช้รูปแบบต่างๆ ของการตั้งค่านี้สำหรับ:

  • การเปลี่ยนระบบภายในอย่างโปร่งใส ("Strangler Pattern")
  • การบังเงาจราจร
  • การรวม API

แสดงรหัสให้ฉันดู

เราจะใช้ GOs ReverseProxy ที่พบในแพ็คเกจ httputil จากไลบรารีมาตรฐานเพื่อใช้งานพรอกซี

Reverse proxy เปิดเผย hooks ที่หลากหลายซึ่งช่วยให้ไคลเอนต์สามารถปรับเปลี่ยนพฤติกรรมของมันได้ พวกเขามาพร้อมกับค่าเริ่มต้นที่สมเหตุสมผล ดังนั้นลูกค้าจึงไม่จำเป็นต้องดำเนินการทุกอย่างด้วยตนเอง

เราจำเป็นต้องแก้ไขคำขอที่เข้ามาและเปลี่ยนเส้นทางตามกฎการแมปบางอย่าง

เพื่อให้บรรลุเป้าหมายนี้ เราจะใช้ฟังก์ชัน Director ของเราเองซึ่งกำหนดไว้เป็น:

Director เป็นฟังก์ชันที่แก้ไขคำขอเป็นคำขอใหม่ที่จะส่งโดยใช้ Transport การตอบสนองของมันจะถูกคัดลอกกลับไปยังไคลเอนต์เดิมโดยไม่มีการแก้ไข

ฟังก์ชัน Director ของเราจะแก้ไขคำขอที่เข้ามาโดยการเปลี่ยน URL ตามคำนำหน้าเส้นทาง URL คำขอที่แก้ไขควรกำหนดเป้าหมาย URL ของระบบย่อยที่ถูกต้องโดยไม่ต้องแก้ไขสิ่งอื่นใด

// director is a function that takes a pointer to http request and modifies it
director := func(req *http.Request) {
  // store original URL
  originalURL := req.URL

  if strings.HasPrefix(originalURL.Path, systemARoutePrefix) {
   req.URL = urlA
  } else if strings.HasPrefix(originalURL.Path, systemBRoutePrefix) {
   req.URL = urlB
  } else {
   return
  }

  // don't forget to take all the URL parts
  req.URL.Fragment = originalURL.Fragment
  req.URL.RawQuery = originalURL.RawQuery
  // map the original path based on some rules
  req.URL.Path = mapPath(originalURL.Path)
 }

นี่คือการใช้งานพร็อกซีแบบเต็ม:

package main

import (
 "fmt"
 "net/http"
 "net/http/httputil"
 "net/url"
 "strings"
)

// URL mapping rules
var subsystemUrlPrefix map[string]string = map[string]string{
 // system A
 "/a/foo/bar": "/v1/foo/bar",
 // system B
 "/b/baz": "/baz",
}

const (
 systemARoutePrefix = "/a"
 systemBRoutePrefix = "/b"
)

// create a new proxy for system A and system B
func NewProxy(systemAURL, systemBURL string) (*httputil.ReverseProxy, error) {
 urlA, urlErr := url.Parse(systemAURL)
 if urlErr != nil {
  return nil, fmt.Errorf("cannot parse system A URL: %w", urlErr)
 }

 urlB, urlErr := url.Parse(systemBURL)
 if urlErr != nil {
  return nil, fmt.Errorf("cannot parse system B URL: %w", urlErr)
 }
 // set up a director function to modify incoming requests
 director := func(req *http.Request) {
  originalURL := req.URL

  if strings.HasPrefix(originalURL.Path, systemARoutePrefix) {
   req.URL = urlA
  } else if strings.HasPrefix(originalURL.Path, systemBRoutePrefix) {
   req.URL = urlB
  } else {
   return
  }

  req.URL.Fragment = originalURL.Fragment
  req.URL.RawQuery = originalURL.RawQuery
  req.URL.Path = mapPath(originalURL.Path)
 }

 return &httputil.ReverseProxy{Director: director}, nil
}

// map path based on the URL prefix
func mapPath(path string) string {
 for apiPrefix, subsystemPrefix := range subsystemUrlPrefix {
  if strings.HasPrefix(path, apiPrefix) {
   return strings.Replace(path, apiPrefix, subsystemPrefix, 1)
  }
 }

 return path
}

การทดสอบ

เราสามารถใช้การทดสอบเพื่อตรวจสอบสิ่งนั้นได้

  • เมื่อได้รับการร้องขอ URL จะต้องได้รับการแก้ไขให้ตรงกับระบบย่อยที่ถูกต้อง
  • เมื่อได้รับการร้องขอแล้ว วิธี HTTP จะไม่ได้รับการแก้ไข

เราจะใช้พร็อกซีฟิกซ์เจอร์ที่ด้านบนของพร็อกซีฟิกซ์เจอร์จริงเพื่อช่วยเราในเรื่องนี้

ขั้นแรก การส่งคำขอ HTTP จริงเพื่อตรวจสอบพฤติกรรมข้างต้นนั้นไม่จำเป็น เราจะตั้งค่าการขนส่งพร็อกซีให้ใช้ noopRoundTripper เพื่อให้แน่ใจว่าการทดสอบจะไม่ทำการโทรผ่านเครือข่าย

ประการที่สอง เราจะกำหนด onOutgoing hook ที่จะอนุญาตให้โค้ดทดสอบตรวจสอบคำขอขาออก

func fixtureProxy(t *testing.T, onOutgoing func(r *http.Request)) *httputil.ReverseProxy {
 p, err := NewProxy(systemABaseUrl, systemBBaseURL)
 require.NoError(t, err)

 originalDirector := p.Director
 p.Director = func(outgoing *http.Request) {
  onOutgoing(outgoing)
  originalDirector(outgoing)
 }
 p.Transport = noopRoundTripper{onRoundTrip: successRoundTrip}
 return p
}

การทดสอบจะสร้างอินสแตนซ์ฟิกซ์เจอร์พร็อกซี ส่งคำขอทดสอบ และตรวจสอบ URL เพื่อให้แน่ใจว่าได้รับการแก้ไขอย่างถูกต้อง

func TestProxy(t *testing.T) {
 testCases := []struct {
  desc             string
  originalPath     string
  originalMethod   string
  expectedProxyURL string
 }{
  {
   desc:             "System A POST",
   originalPath:     "/a/foo/bar",
   originalMethod:   "POST",
   expectedProxyURL: fmt.Sprintf("%s/v1/foo/bar", systemABaseUrl),
  },
  {
   desc:             "System B POST",
   originalPath:     "/b/baz/14",
   originalMethod:   "POST",
   expectedProxyURL: fmt.Sprintf("%s/baz/14", systemBBaseURL),
  },
 }
 for _, tC := range testCases {
  t.Run(tC.desc, func(t *testing.T) {
   var proxiedRequest *http.Request
   p := fixtureProxy(t, func(r *http.Request) {
    proxiedRequest = r
   })

   writer := fixtureWriter()
   req := fixtureRequest(t, tC.originalPath, tC.originalMethod)
   p.ServeHTTP(writer, req)
   require.Equal(t, tC.expectedProxyURL, proxiedRequest.URL.String())
   require.Equal(t, tC.originalMethod, proxiedRequest.Method, "HTTP method should not be modified on proxy")
  })
 }
}

การทดสอบทั้งหมดผ่านไปตามที่คาดไว้ จนถึงตอนนี้ดีมาก

การสังเกตปัญหา

ตอนนี้ได้เวลาเรียกใช้พร็อกซีของเราในการผลิตแล้ว

เพื่อจำลองเงื่อนไขการผลิต เราจะใช้เซิร์ฟเวอร์ HTTP แบบง่ายสองตัวสำหรับบริการ A และบริการ B และเรียกใช้โดยใช้ Docker Compose

บริการทั้งสองจะมีผู้ฟัง HTTP เดียวที่จัดการเส้นทางเป้าหมายของพร็อกซี

package main

import (
 "fmt"
 "net/http"
)

func main() {
 // Return "Hello from service A" when any HTTP request reaches /v1/foo/bar URL
 http.HandleFunc("/v1/foo/bar", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello from service A")
 })
 
 // start the HTTP server running on port 9202
 if err := http.ListenAndServe(":9202", nil); err != nil {
  panic(err)
 }
}

ต่อไป เราจะกำหนด Dockerfiles และหมุนทุกอย่างโดยใช้ Docker Compose (ดูรายละเอียดที่ "พื้นที่เก็บข้อมูล GitHub" เพื่อดูรายละเอียด)

เมื่อระบบเริ่มทำงานแล้ว เราสามารถส่งการรับส่งข้อมูลเพื่อดูว่าพร็อกซีทำงานอย่างไร

การส่งคำขอตามลำดับนั้นทำงานได้อย่างมีเสน่ห์

เมื่อบริการของเราได้รับความนิยมมากขึ้น จำนวนคำขอก็หลั่งไหลเข้ามาเพิ่มขึ้นอย่างรวดเร็ว

เพื่อจำลองสภาพการจราจรสูง เราจะใช้ k6

สคริปต์ k6 จะสุ่มส่งคำขอ HTTP ไปยังเส้นทางที่กำหนดเป้าหมายบริการ A หรือเส้นทางที่กำหนดเป้าหมายบริการ B

import http from 'k6/http';
import { check } from 'k6';

// Testing constants
const SERVICE_A_URL = 'http://localhost:8080/a/foo/bar'
const SERVICE_A_EXPECTED_RESPONSE = 'Hello from service A'
const SERVICE_A_METHOD = "POST"
const SERVICE_B_URL = 'http://localhost:8080/b/baz/14'
const SERVICE_B_EXPECTED_RESPONSE = 'Hello from service B'
const SERVICE_B_METHOD = "GET"

export default function() {
  // Randomly choose between two URLs
  const url = Math.random() > 0.5 ?  SERVICE_A_URL: SERVICE_B_URL;
  const expectedResponse = url === SERVICE_A_URL ? SERVICE_A_EXPECTED_RESPONSE : SERVICE_B_EXPECTED_RESPONSE
  const method = url === SERVICE_A_URL ? SERVICE_A_METHOD : SERVICE_B_METHOD

  // Make the GET request
  const res = http.request(method, url);

  // Check that the response was successful
  check(res, {
    'status is 200': (r) => r.status === 200,
    'OK response': (r)=> r.body === expectedResponse
  });
}

คำขอทั้งสองคาดว่าสถานะการตอบกลับจะเป็น 200 OK และข้อความตอบกลับที่ถูกต้อง

หลังจากรันสคริปต์แล้ว เราพบว่าคำขอเกือบ 50% ล้มเหลว สิ่งที่ช่วยให้?

หากเราปล่อยให้คำขอดำเนินการพร้อมกันในเบื้องหลังและเราลองใช้คำขอด้วยตนเองสองสามรายการ เราจะพบว่าคำขอบางรายการของเราล้มเหลวด้วย 404 Not Found

แม้ว่าเราจะผ่านการทดสอบพฤติกรรมพร็อกซีที่คาดหวังไว้ แต่ระบบของเราจะส่งคืน 404 ในระหว่างสภาวะที่มีภาระงานหนัก

โดยปกติแล้ว วิศวกรจะหมดสติกับปัญหาประเภทนี้ (เนื่องจากฉันทำหลายครั้งเกินไป)

อย่างไรก็ตาม นี่เป็นโพสต์บนบล็อกเกี่ยวกับสภาพการแข่งขัน ดังนั้นคุณคงพอทราบได้ว่าเกิดอะไรขึ้น

เครื่องมือตรวจจับการแข่งขันเพื่อช่วยเหลือ

ระบบนิเวศของ Go มีเครื่องมือมากมายที่ช่วยปรับปรุงประสิทธิภาพการทำงานและช่วยวิศวกรสร้างซอฟต์แวร์ที่แข็งแกร่ง

เครื่องมืออย่างหนึ่งก็คือ Go Race Detector ตามชื่อที่แนะนำ เราจะใช้เครื่องมือเพื่อดูว่าโค้ดของเรามีเงื่อนไขการแข่งขันหรือไม่

เวทมนตร์คอมไพเลอร์ GO แทรกโค้ดที่บันทึกการเข้าถึงหน่วยความจำ ในขณะที่ไลบรารีรันไทม์คอยเฝ้าดูการเข้าถึงตัวแปรที่แชร์โดยไม่ซิงโครไนซ์

ตามเอกสาร:

… เครื่องตรวจจับการแข่งขันสามารถตรวจจับสภาพการแข่งขันได้เฉพาะเมื่อมีการเรียกใช้โค้ดเท่านั้น

มาสร้างการทดสอบด้วยสถานการณ์การทดสอบที่สมจริงซึ่งอาจทำให้สภาพการแข่งขันปรากฏ

การทดสอบจะส่งคำขอพร้อมกัน 100 รายการโดยใช้พร็อกซีฟิกซ์เจอร์ที่อธิบายไว้ก่อนหน้านี้

func TestProxy_ConcurrentRequests(t *testing.T) {
 // create a new fixture proxy
 p := fixtureProxy(t, func(r *http.Request) {})
 // define a new WaitGroup that enables testing code to wait for all 
 // goroutines to finish with their work
 wg := sync.WaitGroup{}

 for i := 0; i < 100; i++ {
  // increment the WaitGroup
  wg.Add(1)
  // start a new goroutine
  go func() {
   // don't forget to decrement the WaitGroup
   defer wg.Done()
   writer := fixtureWriter()
   req := fixtureRequest(t, "/a/foo/bar", "GET")
   // serve the test request with fixture proxy
   p.ServeHTTP(writer, req)
  }()
 }
 
 // wait until all goroutines are done
 wg.Wait()
}

การทดสอบควรทำโดยใช้แฟล็ก -race เพื่อเปิดใช้งานเครื่องตรวจจับสภาพการแข่งขัน

บิงโก! การทดสอบล้มเหลวโดยมีการแข่งขันข้อมูลที่ตรวจพบ 3 ครั้ง เรามาขยายประเด็นและดูว่ามีอะไรผิดพลาดบ้าง

การถอดรหัสเอาต์พุต

เครื่องตรวจจับการแข่งขันจะพิมพ์ร่องรอยสแต็กเพื่ออธิบายสภาพการแข่งขัน ผลลัพธ์สามารถแบ่งออกเป็นสองส่วน:

  1. สแต็กการติดตามสภาพการแข่งขันที่ชี้ไปยังที่อยู่หน่วยความจำและบรรทัดที่เกิดเหตุการณ์นั้น (อะไร/ที่ไหน)
  2. ต้นกำเนิดของ goroutine ที่เกี่ยวข้องกับสภาพการแข่งขัน (ใคร/อย่างไร)

อะไรที่ไหน?

ส่วนแรกของผลลัพธ์จะบอกวิศวกรว่าปัญหาประเภทใดเกิดขึ้น และเกิดขึ้นที่ใด

==================
# What happened
WARNING: DATA RACE 
# Where it happened
Write at 0x00c0001ccbb0 by goroutine 14: 
  github.com/pavisalavisa/race-condition-detection.NewProxy.func1()
      /Users/pavisalavisa/repos/race-condition-detection/proxy.go:47 +0x174 #the problematic write by goroutine 14
  ...

Previous write at 0x00c0001ccbb0 by goroutine 13:
  github.com/pavisalavisa/race-condition-detection.NewProxy.func1()
      /Users/pavisalavisa/repos/race-condition-detection/proxy.go:47 +0x174 #the problematic write by goroutine 13
 ...

เครื่องมือพบว่าเขียนพร้อมกันไปยังที่อยู่หน่วยความจำ 0x00c0001ccbb0 ในบรรทัดที่ 47 ของการใช้งานพร็อกซี

นี่คือบรรทัดภายในฟังก์ชัน Director ที่คัดลอกส่วน URL ดั้งเดิมไปยัง URL พร็อกซี

 director := func(req *http.Request) {
  // Rest of the code

  req.URL.Fragment = originalURL.Fragment // Line 47 DATA RACE
 
  // Rest of the code
 }

ใครอย่างไร?

ส่วนที่สองของผลลัพธ์จะบอกวิศวกรว่ากอร์รูทีนใดบ้างที่เกี่ยวข้อง และพวกมันมีชีวิตขึ้นมาได้อย่างไร:

# Goroutine origins
Goroutine 14 (running) created at:
  github.com/pavisalavisa/race-condition-detection.TestProxy_ConcurrentRequests()
      /Users/pavisalavisa/repos/race-condition-detection/proxy_test.go:92 +0x64
...

Goroutine 13 (finished) created at:
  github.com/pavisalavisa/race-condition-detection.TestProxy_ConcurrentRequests()
      /Users/pavisalavisa/repos/race-condition-detection/proxy_test.go:92 +0x64
  ...

Goroutines ถูกสร้างขึ้นโดยโค้ดทดสอบ ไม่มีอะไรน่าประหลาดใจเลย

กอร์รูทีนเหล่านี้จะถูกสร้างขึ้นโดยไลบรารี HTTP หากมีการใช้งานแอปพลิเคชันซึ่งนำไปสู่ความล้มเหลวแบบเดียวกัน

ในเซิร์ฟเวอร์ Go แต่ละคำขอที่เข้ามาจะได้รับการจัดการใน goroutine ของตัวเอง ("แหล่งที่มา")

เครื่องมือตรวจจับการแข่งขันสามารถใช้เพื่อตรวจสอบแอปพลิเคชันที่ทำงานอยู่ได้เช่นกัน โดยเรียกใช้บริการด้วยแฟล็ก -race

โปรดใช้ความระมัดระวังในการทดลองคุณลักษณะนี้ในการใช้งานจริงเนื่องจาก

ค่าใช้จ่ายในการตรวจจับการแข่งขันจะแตกต่างกันไปตามโปรแกรม แต่สำหรับโปรแกรมทั่วไป การใช้หน่วยความจำอาจเพิ่มขึ้น 5–10x และเวลาดำเนินการ 2–20x ("แหล่งที่มา")

แก้ไขปัญหา

ตอนนี้เรามีความเข้าใจในเรื่อง Data Race แล้ว มาดูกันว่าเราทำอะไรผิดในฟังก์ชัน Director บ้าง

 director := func(req *http.Request) {
  originalURL := req.URL

  if strings.HasPrefix(originalURL.Path, systemARoutePrefix) {
   req.URL = urlA
  } else if strings.HasPrefix(originalURL.Path, systemBRoutePrefix) {
   req.URL = urlB
  } else {
   return
  }

  req.URL.Fragment = originalURL.Fragment
  req.URL.RawQuery = originalURL.RawQuery
  req.URL.Path = mapPath(originalURL.Path)
 }

ฟังก์ชันผู้อำนวยการอัปเดตส่วน URL คำขอพร็อกซีด้วยข้อมูลคำขอดั้งเดิม การเขียนลงในฟิลด์ Fragment struct พร้อมกันบ่งชี้ว่ามี goroutines หลายรายการสามารถเข้าถึง URL เดียวกัน

URL ของพร็อกซีถูกสร้างอินสแตนซ์ด้วยฟังก์ชัน url.Parse ซึ่งจะส่งคืน ตัวชี้ ไปยัง URL เมื่อสตริงที่ระบุเป็น URL ที่ถูกต้อง

> go doc net/url URL.Parse

func (u *URL) Parse(ref string) (*URL, error)
    Parse parses a URL in the context of the receiver. The provided URL may be
    relative or absolute. Parse returns nil, err on parse failure, otherwise its
    return value is the same as ResolveReference.

URL เหล่านี้จะถูกแยกวิเคราะห์เพียงครั้งเดียวเมื่อเริ่มต้นเมื่อสร้างพร็อกซีใหม่ ด้วยเหตุนี้ ทุกคำขอจึงใช้ตัวชี้ URL เดียวกันและแก้ไขข้อมูลพื้นฐาน

func NewProxy(systemAURL, systemBURL string) (*httputil.ReverseProxy, error) {
 urlA, urlErr := url.Parse(systemAURL)
 if urlErr != nil {
  return nil, fmt.Errorf("cannot parse system A URL: %w", urlErr)
 }

  // Rest of the code
}

นั่นเป็นปัญหาเล็กน้อย ไม่ต้องกังวลเพราะเรามี (อย่างน้อย) สองทางเลือกในการแก้ไข:

  1. โคลน URL พร็อกซี
  2. หลีกเลี่ยงการแก้ไขตัวชี้ URL

เรามาดำเนินการกับตัวเลือกที่หนึ่งแล้วดูว่าจะเป็นอย่างไร

ตาม “การสนทนา GitHub” ใน repo GO อย่างเป็นทางการ การโคลน URL ทำได้อย่างปลอดภัยโดยการยกเลิกการอ้างอิงตัวชี้

 director := func(req *http.Request) {
  // store the original URL
  originalURL := req.URL
  var proxyURL url.URL

  if strings.HasPrefix(originalURL.Path, systemARoutePrefix) {
   proxyURL = *urlA // dereference the parsed urlA to ensure we get a copy
  } else if strings.HasPrefix(originalURL.Path, systemBRoutePrefix) {
   proxyURL = *urlB // dereference the parsed urlB to ensure we get a copy
  } else {
   return
  }
  
  req.URL = &proxyURL
  req.URL.Fragment = originalURL.Fragment
  req.URL.RawQuery = originalURL.RawQuery
  req.URL.Path = mapPath(originalURL.Path)
 }

หลังจากทำการเปลี่ยนแปลงข้างต้นแล้ว เราก็สามารถพูดได้อย่างภาคภูมิใจว่าปัญหาได้รับการแก้ไขแล้ว! การทดสอบของเราผ่านการทดสอบแล้ว และเราสามารถปรับใช้บริการของเราได้อย่างมั่นใจ

ซื้อกลับบ้าน

สภาพการแข่งขันเป็นข้อผิดพลาดในการเขียนโปรแกรมที่เข้าใจยากและเป็นอันตรายซึ่งคงอยู่ตลอดหลายทศวรรษ

ความเสียหายที่อาจเกิดขึ้นนั้นแตกต่างกันไปตั้งแต่อันตรายเป็นศูนย์ในโครงการด้านการศึกษาและของเล่นไปจนถึงการสูญเสียชีวิตในกรณีร้ายแรง

เราได้แสดงให้เห็นแล้วว่าแม้แต่บริการที่มีโค้ดไม่เกินร้อยบรรทัดก็สามารถประสบปัญหาได้

หากคุณกำลังสร้างโครงสร้างพื้นฐานที่สำคัญสำหรับระบบของคุณ อย่าปล่อยให้ความเรียบง่ายหลอกลวงคุณ

โยนลูกบอลโค้งไปที่ระบบของคุณ หรือดีกว่านั้นคือโยนลูกบอลโค้งหลายพันลูกต่อวินาที

แม้ว่าการทดสอบในห้องปฏิบัติการจะช่วยให้คุณตรวจพบแมลงได้ แต่อย่าคาดหวังว่ามันจะสมบูรณ์แบบ การผลิตเป็นสัตว์ร้ายที่แตกต่างไปจากเดิมอย่างสิ้นเชิง และคุณควรเตรียมพร้อมสำหรับมัน

เมื่อสิ่งต่างๆ แย่ลง (และจะ) ตรวจสอบให้แน่ใจว่าคุณได้ตั้งค่าความสามารถในการสังเกตอย่างเหมาะสม การดีบักแบบคนตาบอดเป็นงานของคนโง่

สุดท้ายก็พาเพื่อน ๆ มาร่วมขี่ด้วย บางครั้ง แค่เพียงมีดวงตาคู่ใหม่ก็เพียงพอที่จะมองเห็นสภาพการแข่งขันที่ยากลำบากได้ มันเป็นประสบการณ์ที่ผูกพันกันมาก แถมยังสนุกกว่าการแก้ปัญหากับเพื่อนอีกด้วย

โดยย่อ:

  • ทดสอบความเครียดระบบของคุณ
  • ดูที่เคสขอบ
  • ใช้เครื่องมือพิเศษ
  • พาเพื่อนฝูงมาช่วยคุณ
  • และอย่าลืมสนุกไปกับมันด้วย ท้ายที่สุดแล้ว หากคุณไม่เพลิดเพลินกับกระบวนการ จะมีประโยชน์อะไร?

คุณสามารถดูตัวอย่างโค้ดได้ที่ GitHub

การบันทึกเทอร์มินัลแบบเคลื่อนไหวถูกสร้างขึ้นด้วย เทอร์มินัล