Skip to content

Commit 34316b0

Browse files
authored
perf: improve performance (#124)
* perf: improve performance * feat: fix ordering issue in test and support acking * feat: support multiple instances to improve perf
1 parent f7105d9 commit 34316b0

File tree

15 files changed

+275
-79
lines changed

15 files changed

+275
-79
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1+
<!--
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software
9+
distributed under the License is distributed on an "AS IS" BASIS,
10+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
See the License for the specific language governing permissions and
12+
limitations under the License.
13+
-->
14+
115
# function-stream

benchmark/bench_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@ package benchmark
1616

1717
import (
1818
"context"
19+
"github.com/apache/pulsar-client-go/pulsaradmin"
20+
"github.com/apache/pulsar-client-go/pulsaradmin/pkg/utils"
1921
"github.com/functionstream/functionstream/common"
2022
"github.com/functionstream/functionstream/perf"
23+
"github.com/functionstream/functionstream/restclient"
2124
"github.com/functionstream/functionstream/server"
2225
"io"
2326
"log/slog"
27+
"math/rand"
2428
"os"
2529
"runtime/pprof"
30+
"strconv"
2631
"testing"
2732
"time"
2833
)
@@ -50,9 +55,37 @@ func BenchmarkStressForBasicFunc(b *testing.B) {
5055
})
5156
}()
5257

58+
inputTopic := "test-input-" + strconv.Itoa(rand.Int())
59+
outputTopic := "test-output-" + strconv.Itoa(rand.Int())
60+
cfg := &pulsaradmin.Config{}
61+
admin, err := pulsaradmin.NewClient(cfg)
62+
if err != nil {
63+
panic(err)
64+
}
65+
replicas := int32(5)
66+
createTopic := func(t string) {
67+
tn, err := utils.GetTopicName(t)
68+
if err != nil {
69+
panic(err)
70+
}
71+
err = admin.Topics().Create(*tn, int(replicas))
72+
if err != nil {
73+
panic(err)
74+
}
75+
76+
}
77+
createTopic(inputTopic)
78+
createTopic(outputTopic)
79+
5380
pConfig := perf.Config{
5481
PulsarURL: "pulsar://localhost:6650",
55-
RequestRate: 500.0,
82+
RequestRate: 100000.0,
83+
Func: &restclient.Function{
84+
Archive: "./bin/example_basic.wasm",
85+
Inputs: []string{inputTopic},
86+
Output: outputTopic,
87+
Replicas: &replicas,
88+
},
5689
}
5790

5891
b.ReportAllocs()

common/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ import "errors"
1818

1919
var (
2020
ErrorFunctionNotFound = errors.New("function not found")
21+
ErrorFunctionExists = errors.New("function already exists")
2122
)

common/model/function.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
package model
1616

1717
type Function struct {
18-
Name string `json:"name"`
19-
Archive string `json:"archive"`
20-
Inputs []string `json:"inputs"`
21-
Output string `json:"output"`
22-
Config map[string]string `json:"config"`
18+
Name string
19+
Archive string
20+
Inputs []string
21+
Output string
22+
Config map[string]string
23+
Replicas int32
2324
}

examples/basic/main.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ import (
2222
)
2323

2424
type Person struct {
25-
Name string `json:"name"`
26-
Money int `json:"money"`
25+
Name string `json:"name"`
26+
Money int `json:"money"`
27+
Expected int `json:"expected"`
2728
}
2829

2930
func main() {
3031
_, _ = fmt.Fprintln(os.Stderr, "Hello from Go!")
32+
}
3133

34+
//export process
35+
func process() {
3236
dataBytes, err := io.ReadAll(os.Stdin)
3337
if err != nil {
3438
_, _ = fmt.Fprintln(os.Stderr, "Failed to read data:", err)

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/functionstream/functionstream
33
go 1.21
44

55
require (
6-
github.com/apache/pulsar-client-go v0.11.1
6+
github.com/apache/pulsar-client-go v0.12.0
77
github.com/bmizerany/perks v0.0.0-20230307044200-03f9df79da1e
88
github.com/gorilla/mux v1.8.1
99
github.com/pkg/errors v0.9.1
@@ -33,6 +33,8 @@ require (
3333
github.com/google/gofuzz v1.2.0 // indirect
3434
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
3535
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
36+
github.com/hashicorp/errwrap v1.0.0 // indirect
37+
github.com/hashicorp/go-multierror v1.1.1 // indirect
3638
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3739
github.com/json-iterator/go v1.1.12 // indirect
3840
github.com/klauspost/compress v1.17.5 // indirect

go.sum

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ github.com/AthenZ/athenz v1.11.50 h1:mCyQhI32GHPpPde9NVChI46hpRjw+vX1Z4RN8GCDILE
66
github.com/AthenZ/athenz v1.11.50/go.mod h1:HfKWur/iDpTKNb2TVaKKy4mt+Qa0PnZpIOqcmR9/i+Q=
77
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
88
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
9-
github.com/apache/pulsar-client-go v0.11.1 h1:WxLitlPG4Dz62BblGlx51wm0rw76eRefJsWdawI22QM=
10-
github.com/apache/pulsar-client-go v0.11.1/go.mod h1:FoijqJwgjroSKptIWp1vvK1CXs8dXnQiL8I+MHOri4A=
9+
github.com/apache/pulsar-client-go v0.12.0 h1:rrMlwpr6IgLRPXLRRh2vSlcw5tGV2PUSjZwmqgh2B2I=
10+
github.com/apache/pulsar-client-go v0.12.0/go.mod h1:dkutuH4oS2pXiGm+Ti7fQZ4MRjrMPZ8IJeEGAWMeckk=
1111
github.com/ardielle/ardielle-go v1.5.2 h1:TilHTpHIQJ27R1Tl/iITBzMwiUGSlVfiVhwDNGM3Zj4=
1212
github.com/ardielle/ardielle-go v1.5.2/go.mod h1:I4hy1n795cUhaVt/ojz83SNVCYIGsAFAONtv2Dr7HUI=
1313
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -63,6 +63,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
6363
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
6464
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
6565
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
66+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
67+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
68+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
69+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
6670
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
6771
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6872
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=

lib/instance.go

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/functionstream/functionstream/common"
2222
"github.com/functionstream/functionstream/common/model"
2323
"github.com/pkg/errors"
24+
"github.com/sirupsen/logrus"
2425
"github.com/tetratelabs/wazero"
2526
"github.com/tetratelabs/wazero/api"
2627
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
@@ -32,19 +33,25 @@ import (
3233
type FunctionInstance struct {
3334
ctx context.Context
3435
cancelFunc context.CancelFunc
35-
definition model.Function
36+
definition *model.Function
3637
pc pulsar.Client
3738
readyCh chan error
39+
index int32
3840
}
3941

40-
func NewFunctionInstance(definition model.Function, pc pulsar.Client) *FunctionInstance {
42+
func NewFunctionInstance(definition *model.Function, pc pulsar.Client, index int32) *FunctionInstance {
4143
ctx, cancelFunc := context.WithCancel(context.Background())
44+
ctx.Value(logrus.Fields{
45+
"function-name": definition.Name,
46+
"function-index": index,
47+
})
4248
return &FunctionInstance{
4349
ctx: ctx,
4450
cancelFunc: cancelFunc,
4551
definition: definition,
4652
pc: pc,
4753
readyCh: make(chan error),
54+
index: index,
4855
}
4956
}
5057

@@ -69,7 +76,7 @@ func (instance *FunctionInstance) Run() {
6976
stdout := common.NewChanWriter()
7077

7178
config := wazero.NewModuleConfig().
72-
WithStdout(stdout).WithStdin(stdin)
79+
WithStdout(stdout).WithStdin(stdin).WithStderr(os.Stderr)
7380

7481
wasi_snapshot_preview1.MustInstantiate(instance.ctx, r)
7582

@@ -82,6 +89,7 @@ func (instance *FunctionInstance) Run() {
8289
consumer, err := instance.pc.Subscribe(pulsar.ConsumerOptions{
8390
Topics: instance.definition.Inputs,
8491
SubscriptionName: fmt.Sprintf("function-stream-%s", instance.definition.Name),
92+
Type: pulsar.Failover,
8593
})
8694
if err != nil {
8795
instance.readyCh <- errors.Wrap(err, "Error creating consumer")
@@ -102,8 +110,6 @@ func (instance *FunctionInstance) Run() {
102110
producer.Close()
103111
}()
104112

105-
instance.readyCh <- nil
106-
107113
handleErr := func(ctx context.Context, err error, message string, args ...interface{}) {
108114
if errors.Is(err, context.Canceled) {
109115
slog.InfoContext(instance.ctx, "function instance has been stopped")
@@ -112,6 +118,24 @@ func (instance *FunctionInstance) Run() {
112118
slog.ErrorContext(ctx, message, args...)
113119
}
114120

121+
// Trigger the "_start" function, WASI's "main".
122+
mod, err := r.InstantiateWithConfig(instance.ctx, wasmBytes, config)
123+
if err != nil {
124+
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
125+
handleErr(instance.ctx, err, "Function exit with code", "code", exitErr.ExitCode())
126+
} else if !ok {
127+
handleErr(instance.ctx, err, "Error instantiating function")
128+
}
129+
return
130+
}
131+
process := mod.ExportedFunction("process")
132+
if process == nil {
133+
instance.readyCh <- errors.New("No process function found")
134+
return
135+
}
136+
137+
instance.readyCh <- nil
138+
115139
for {
116140
msg, err := consumer.Receive(instance.ctx)
117141
if err != nil {
@@ -120,14 +144,9 @@ func (instance *FunctionInstance) Run() {
120144
}
121145
stdin.ResetBuffer(msg.Payload())
122146

123-
// Trigger the "_start" function, WASI's "main".
124-
_, err = r.InstantiateWithConfig(instance.ctx, wasmBytes, config)
147+
_, err = process.Call(instance.ctx)
125148
if err != nil {
126-
if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 {
127-
handleErr(instance.ctx, err, "Function exit with code", "code", exitErr.ExitCode())
128-
} else if !ok {
129-
handleErr(instance.ctx, err, "Error instantiating function")
130-
}
149+
handleErr(instance.ctx, err, "Error calling process function")
131150
return
132151
}
133152

@@ -139,16 +158,17 @@ func (instance *FunctionInstance) Run() {
139158
handleErr(instance.ctx, err, "Error sending message", "error", err, "messageId", id)
140159
return
141160
}
161+
err = consumer.Ack(msg)
162+
if err != nil {
163+
handleErr(instance.ctx, err, "Error acknowledging message", "error", err, "messageId", id)
164+
return
165+
}
142166
})
143167
}
144168
}
145169

146-
func (instance *FunctionInstance) WaitForReady() error {
147-
err := <-instance.readyCh
148-
if err != nil {
149-
slog.ErrorContext(instance.ctx, "Error starting function instance", err)
150-
}
151-
return err
170+
func (instance *FunctionInstance) WaitForReady() chan error {
171+
return instance.readyCh
152172
}
153173

154174
func (instance *FunctionInstance) Stop() {

lib/manager.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import (
1818
"github.com/apache/pulsar-client-go/pulsar"
1919
"github.com/functionstream/functionstream/common"
2020
"github.com/functionstream/functionstream/common/model"
21+
"log/slog"
2122
"sync"
2223
)
2324

2425
type FunctionManager struct {
25-
functions map[string]*FunctionInstance
26+
functions map[string][]*FunctionInstance
2627
functionsLock sync.Mutex
2728
pc pulsar.Client
2829
}
@@ -35,29 +36,43 @@ func NewFunctionManager() (*FunctionManager, error) {
3536
return nil, err
3637
}
3738
return &FunctionManager{
38-
functions: make(map[string]*FunctionInstance),
39+
functions: make(map[string][]*FunctionInstance),
3940
pc: pc,
4041
}, nil
4142
}
4243

43-
func (fm *FunctionManager) StartFunction(f model.Function) error {
44+
func (fm *FunctionManager) StartFunction(f *model.Function) error {
4445
fm.functionsLock.Lock()
4546
defer fm.functionsLock.Unlock()
46-
instance := NewFunctionInstance(f, fm.pc)
47-
fm.functions[f.Name] = instance
48-
go instance.Run()
49-
return instance.WaitForReady()
47+
if _, exist := fm.functions[f.Name]; exist {
48+
fm.functionsLock.Unlock()
49+
return common.ErrorFunctionExists
50+
}
51+
fm.functions[f.Name] = make([]*FunctionInstance, f.Replicas)
52+
for i := int32(0); i < f.Replicas; i++ {
53+
instance := NewFunctionInstance(f, fm.pc, i)
54+
fm.functions[f.Name][i] = instance
55+
go instance.Run()
56+
if err := <-instance.WaitForReady(); err != nil {
57+
if err != nil {
58+
slog.ErrorContext(instance.ctx, "Error starting function instance", err)
59+
}
60+
fm.functionsLock.Unlock()
61+
return err
62+
}
63+
}
64+
return nil
5065
}
5166

5267
func (fm *FunctionManager) DeleteFunction(name string) error {
5368
fm.functionsLock.Lock()
54-
instance, exist := fm.functions[name]
69+
instances, exist := fm.functions[name]
5570
if !exist {
5671
return common.ErrorFunctionNotFound
5772
}
5873
delete(fm.functions, name)
5974
fm.functionsLock.Unlock()
60-
if instance != nil {
75+
for _, instance := range instances {
6176
instance.Stop()
6277
}
6378
return nil

openapi.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ components:
8181
type: string
8282
output:
8383
type: string
84+
replicas:
85+
type: integer
8486
config:
8587
type: object
8688
additionalProperties:

0 commit comments

Comments
 (0)